diff --git a/README.md b/README.md index 70130faf..48153eb8 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ **把你自己的登录态,变成一个本机 OpenAI 兼容 API** -接入 **Qoder(国际版 / 国内版)**、**WorkBuddy(国际版 / 国内版)**、**Trae 国内版 Work**,以及实验性的 **Devin** 和 **Command Code**。 +接入 **Qoder(国际版 / 国内版)**、**WorkBuddy(国际版 / 国内版)**、**Trae 国内版 Work**,以及实验性的 **Devin**、**Command Code** 和 **华为云码道 CodeArts**。 用 Docker 部署,在 Web 控制台管理账号,再通过兼容 API 接入客户端。 @@ -26,9 +26,10 @@ - **兼容接口**:Chat Completions、Responses、Anthropic Messages 和模型列表,支持流式回复与函数工具调用。 - **多账号调度**:轮询、加权轮询、顺序填满,以及可选的同供应商最早到期优先(FEFO);保留自动选号、会话粘性、并发限制、冷却与故障切换。 - **统一控制台**:管理账号、模型、客户端密钥、代理设置,查看额度和请求日志。 +- **华为云码道(实验性)**:OAuth PKCE 登录后直连 snap-access 的 OpenAI 兼容接口,支持流式、工具调用、按目录声明向视觉模型转发图片、按目录声明的 reasoning 档位,并读取套餐积分 / token 计量。 - **Docker 运维**:持久化账号数据;可选安装宿主机更新器,在控制台下载更新、确认升级和回滚。 -Qoder 国内版、WorkBuddy、Trae 的适配代码已实现,真账号验收仍未完成;Devin 与 Command Code 为实验性接入,不承诺生产可用。托管更新的真实升级 / 回滚验收也尚未完成。Kimi Code(中国区)已完成单账号端到端验收:设备码登录、模型发现、三种接口的对话 / 流式 / 工具调用 / 视觉,以及额度读取;多账号行为只有自动化测试覆盖。 +Qoder 国内版、WorkBuddy、Trae 的适配代码已实现,真账号验收仍未完成;Devin、Command Code 与华为云码道 CodeArts 为实验性接入,不承诺生产可用。托管更新的真实升级 / 回滚验收也尚未完成。Kimi Code(中国区)已完成单账号端到端验收:设备码登录、模型发现、三种接口的对话 / 流式 / 工具调用 / 视觉,以及额度读取;多账号行为只有自动化测试覆盖。码道 CodeArts 已完成单账号端到端验收:OAuth 登录、模型发现与不可用模型剔除、Chat Completions / Anthropic Messages / Responses 的非流式与流式、工具调用、reasoning 透传、套餐额度读取和签到;双账号失败切换与会话粘性也已实测,额度耗尽切换仍待验收。 ## 快速开始 @@ -63,7 +64,7 @@ API Key: <在 API keys 页面创建的客户端密钥> CLI2API 架构:OpenAI 客户端经 Go 控制面路由到每账号独立运行时,再连接各 provider 上游

-Go 网关统一鉴权、调度和记录请求。Qoder 每个账号使用独立 Node 进程与 HOME;WorkBuddy、Trae、Devin、Command Code 使用 Go 进程内适配器,不为每次请求启动完整 CLI。 +Go 网关统一鉴权、调度和记录请求。Qoder 每个账号使用独立 Node 进程与 HOME;WorkBuddy、Trae、Devin、Command Code 使用 Go 进程内适配器,不为每次请求启动完整 CLI;码道 CodeArts 也走进程内适配器,直连华为云 snap-access 的 OpenAI 兼容网关,不拉起码道 CLI 或 IDE。 ## 控制台 @@ -82,13 +83,13 @@ Go 网关统一鉴权、调度和记录请求。Qoder 每个账号使用独立 N - **只在同一供应商内排序**:先按供应商分组,再比较本组账号最近的积分包到期日;Qoder、WorkBuddy 等互不影响,跨供应商仍按原有轮询选择。 - **只认实际可消费的积分包到期**:跳过余额为零、已过期或到期未知的包;同一 UTC+8 自然日到期的账号公平轮询。未知到期不猜测,作为该供应商的兜底候选,只有已知到期账号都不可用时才参与。 - **额度与故障切换照旧**:额度耗尽、限流冷却、并发占满或被显式排除的账号先退出候选,恢复后自动回到排序。响应或流结束后异步刷新该账号额度并重新排序;缓存中的包自然过期也会触发额度探测。不会把订阅重置、令牌有效期或签到奖励当成积分包到期。 -- **数据缺口**:目前只有 WorkBuddy 个人账号的 `get-user-resource` 返回逐包 `CycleEndTime`。Qoder 的 `quota/usage` 只有订阅月度边界(`expiresAt`),Trae、Devin、Command Code、Codex 也只有重置窗口或总额度,因此这些供应商的账号在 FEFO 下按未知日期处理。WorkBuddy 企业/共享身份,以及结果达到 100 条的未确认列表,也不会获得 FEFO 优先级。升级后尚未刷新额度的旧缓存同样按未知日期处理,启动探测或手动刷新后才会带上包元数据。 +- **数据缺口**:目前只有 WorkBuddy 个人账号的 `get-user-resource` 返回逐包 `CycleEndTime`。Qoder 的 `quota/usage` 只有订阅月度边界(`expiresAt`),Trae、Devin、Command Code、Codex 也只有重置窗口或总额度,因此这些供应商的账号在 FEFO 下按未知日期处理。码道 CodeArts 的统计接口提供套餐积分与 token 计量,但不提供逐包到期日,同样按未知日期兜底,不伪造到期。WorkBuddy 企业/共享身份,以及结果达到 100 条的未确认列表,也不会获得 FEFO 优先级。升级后尚未刷新额度的旧缓存同样按未知日期处理,启动探测或手动刷新后才会带上包元数据。 ## 使用限制 - 只连接你自己的账号,不提供账号、额度或官方 API 服务。 - 兼容接口不等于完整复刻官方 API。Messages / Responses 为无状态适配,不保存服务端会话,也不执行上游专属工具。 -- 图片能力取决于上游和模型,网关把上游声明的图片输入逐模型透出为 `supports_images`(`/v1/models` 与 `/api/models`)。WorkBuddy 的 DeepSeek V4 / V4.1、GLM、Kimi 等模型支持图片;Trae、Command、Zhipu 未声明图片输入。具体范围见 [部署说明](deploy/README.md)。 +- 图片能力取决于上游和模型,网关把上游声明的图片输入逐模型透出为 `supports_images`(`/v1/models` 与 `/api/models`)。WorkBuddy 的 DeepSeek V4 / V4.1、GLM、Kimi 等模型支持图片;码道的 Qwen3-VL-235B 与 Kimi K2.6 VL 支持图片,其余码道模型按文本处理;Trae、Command、Zhipu 未声明图片输入。具体范围见 [部署说明](deploy/README.md)。 - 跨上游同名模型调度受系统开关、账号权限和地域约束;不是任意账号之间都能切换。 - 上游协议变化可能影响兼容性;请求日志默认不保存提示词或回复正文。 diff --git a/README_EN.md b/README_EN.md index 293d3e97..430f2619 100644 --- a/README_EN.md +++ b/README_EN.md @@ -4,7 +4,7 @@ **Turn your own logins into a local OpenAI-compatible API** -Connect **Qoder (Global / CN)**, **WorkBuddy (Global / CN)**, **Trae CN Work**, and experimental **Devin** and **Command Code** accounts. +Connect **Qoder (Global / CN)**, **WorkBuddy (Global / CN)**, **Trae CN Work**, and experimental **Devin**, **Command Code**, and **Huawei Cloud CodeArts (码道)** accounts. Deploy with Docker, manage accounts in the web console, and connect clients through compatible APIs. @@ -26,9 +26,10 @@ Deploy with Docker, manage accounts in the web console, and connect clients thro - **Compatible APIs**: Chat Completions, Responses, Anthropic Messages, and model listing, with streaming and function tools. - **Multi-account routing**: round-robin, weighted round-robin, fill-first, and opt-in same-provider earliest-expiry-first (FEFO), with automatic selection, session affinity, concurrency limits, cooldowns, and failover. - **Web console**: manage accounts, models, client keys, and proxies; view quotas and request logs. +- **Huawei Cloud CodeArts (experimental)**: OAuth PKCE sign-in followed by direct OpenAI-compatible calls to snap-access, with streaming, tools, image input for catalog-declared vision models, catalog-declared reasoning levels, and package-credit / token meters. - **Docker operations**: persistent account data; an optional host updater lets you download, confirm, and roll back updates from the console. -Qoder CN, WorkBuddy, and Trae adapters are implemented, but live-account acceptance is still pending. Devin and Command Code are experimental, not production-ready. Live upgrade and rollback acceptance is also pending. The Kimi Code CN adapter has single-account end-to-end acceptance: device-code sign-in, model discovery, chat/streaming/tools/vision on all three APIs, and quota readout; multi-account behavior is covered by automated tests only. +Qoder CN, WorkBuddy, and Trae adapters are implemented, but live-account acceptance is still pending. Devin, Command Code, and Huawei Cloud CodeArts are experimental, not production-ready. Live upgrade and rollback acceptance is also pending. The Kimi Code CN adapter has single-account end-to-end acceptance: device-code sign-in, model discovery, chat/streaming/tools/vision on all three APIs, and quota readout; multi-account behavior is covered by automated tests only. CodeArts has single-account end-to-end acceptance: OAuth sign-in, model discovery with unusable-model removal, non-streaming and streaming Chat Completions / Anthropic Messages / Responses, tool calls, reasoning passthrough, package-credit readout, and check-in; two-account failover and session affinity have also been verified live, while quota-exhaustion failover is still pending. ## Quick start @@ -63,7 +64,7 @@ Get a model ID from **Access** or `/v1/models`. Routing is automatic and prefers CLI2API architecture: OpenAI clients are routed by the Go control plane to one isolated runtime per account, then to the provider upstream

-The Go gateway handles authentication, routing, and request logging. Each Qoder account has its own Node process and HOME; WorkBuddy, Trae, Devin, and Command Code use Go in-process adapters. No full CLI is started per request. +The Go gateway handles authentication, routing, and request logging. Each Qoder account has its own Node process and HOME; WorkBuddy, Trae, Devin, Command Code, and CodeArts use Go in-process adapters. No full CLI is started per request. ## Console @@ -82,13 +83,13 @@ Select **Earliest expiry first (FEFO)** under **System → Account routing strat - **Rank only inside one provider.** Accounts are grouped by provider family before their nearest credit-package expiry is compared. Qoder, WorkBuddy, and other providers never compete with each other, and cross-provider selection keeps its existing rotation. - **Use only spendable package dates.** Depleted, expired, and undated packages are skipped. Accounts expiring on the same UTC+8 calendar day rotate fairly. Missing dates are never guessed: they form that provider's fallback cohort and participate only after every dated account is unavailable. - **Keep quota and failover semantics.** Exhausted, cooling, concurrency-saturated, or explicitly excluded accounts leave the candidate set and return to the ranking once they recover. Finishing a response or stream refreshes that account's quota asynchronously; a cached package expiring naturally also triggers a probe. Subscription resets, token lifetimes, and check-in rewards are never treated as package expiry. -- **Data gaps.** Only WorkBuddy personal accounts currently return per-package `CycleEndTime` from `get-user-resource`. Qoder's `quota/usage` exposes only the subscription cycle boundary (`expiresAt`), while Trae, Devin, Command Code, and Codex expose reset windows or totals only, so their accounts remain in the unknown-date cohort under FEFO. WorkBuddy enterprise/shared identities and unconfirmed result sets capped at 100 also receive no FEFO priority. Cached quota snapshots written before this version remain undated until the boot probe or a manual refresh repopulates the package metadata. +- **Data gaps.** Only WorkBuddy personal accounts currently return per-package `CycleEndTime` from `get-user-resource`. Qoder's `quota/usage` exposes only the subscription cycle boundary (`expiresAt`), while Trae, Devin, Command Code, and Codex expose reset windows or totals only, so their accounts remain in the unknown-date cohort under FEFO. CodeArts reports package credits and token meters but no per-package expiry, so it is treated as unknown rather than given a fabricated date. WorkBuddy enterprise/shared identities and unconfirmed result sets capped at 100 also receive no FEFO priority. Cached quota snapshots written before this version remain undated until the boot probe or a manual refresh repopulates the package metadata. ## Limitations - Bring your own accounts. CLI2API does not supply accounts, quotas, or an official API service. - Compatibility is not full API parity. Messages / Responses are stateless adapters, without server-side conversations or upstream-specific tool execution. -- Image support depends on the upstream and model. The gateway surfaces each model's declared image input as `supports_images` on `/v1/models` and `/api/models`. WorkBuddy's DeepSeek V4 / V4.1, GLM, and Kimi models accept images; Trae, Command, and Zhipu do not declare image input. See the [deployment guide](deploy/README.md) for details. +- Image support depends on the upstream and model. The gateway surfaces each model's declared image input as `supports_images` on `/v1/models` and `/api/models`. WorkBuddy's DeepSeek V4 / V4.1, GLM, and Kimi models accept images; CodeArts's Qwen3-VL-235B and Kimi K2.6 VL accept images, while its other models are text-only; Trae, Command, and Zhipu do not declare image input. See the [deployment guide](deploy/README.md) for details. - Cross-provider routing for shared model IDs follows system settings, key grants, and region constraints; failover is not unrestricted. - Upstream changes can break compatibility. Request logs do not store prompt or completion bodies by default. diff --git a/changelog/unreleased/codearts-provider.md b/changelog/unreleased/codearts-provider.md new file mode 100644 index 00000000..a6d7df5d --- /dev/null +++ b/changelog/unreleased/codearts-provider.md @@ -0,0 +1,7 @@ +### English + +- Add the experimental Huawei Cloud CodeArts Agent (码道) provider: OAuth PKCE sign-in, direct OpenAI-compatible chat and SSE, catalog discovery with unusable-model removal, image input for catalog-declared vision models, catalog-declared reasoning levels, quota meters, and daily/new-user credit check-in. Single-account end-to-end acceptance and two-account failover/session affinity are complete; quota-exhaustion failover remains pending. + +### 中文 + +- 新增实验性的华为云码道 CodeArts 供应商:OAuth PKCE 登录、直连 OpenAI 兼容对话与 SSE、模型目录并剔除账号不可用模型、按目录声明向视觉模型转发图片、按目录声明暴露 reasoning 档位、额度计量,以及每日 / 新用户积分签到。单账号端到端验收、双账号失败切换与会话粘性已完成;额度耗尽切换仍待验收。 diff --git a/docs/ARCHITECTURE_SUMMARY.md b/docs/ARCHITECTURE_SUMMARY.md index ef434cf2..a6e691f0 100644 --- a/docs/ARCHITECTURE_SUMMARY.md +++ b/docs/ARCHITECTURE_SUMMARY.md @@ -6,7 +6,7 @@ status: canonical read-when: 需要快速理解后端分层、请求路径、运行时和 provider 扩展边界时 summary: 面向贡献者和 AI 的精简架构摘要;详细协议、里程碑和本地运维资料不在本文维护。 related: [AGENTS.md, CONTRIBUTING.md, docs/DESIGN.md, docs/REFACTORING.md, docs/NATIVE_PROTOCOL_REQUESTS.md] -last-updated: 2026-09-24 +last-updated: 2026-10-29 --- # CLI2API 架构摘要 @@ -20,7 +20,7 @@ CLI2API 是一个面向个人部署的 Go + Node 网关: - 对外提供 OpenAI Chat Completions、Anthropic Messages、OpenAI Responses 和 Models 兼容接口。 - 管理多个上游账号,并按 provider、region、model、pin、会话粘性和冷却状态选号。 -- Qoder 登录、目录和配额仍使用每账号隔离的 Node child process;Qoder 聊天在 Go 进程内签名直连网关。WorkBuddy、Trae CN Work 和 Devin 使用 Go 进程内 adapter。 +- Qoder 登录、目录和配额仍使用每账号隔离的 Node child process;Qoder 聊天在 Go 进程内签名直连网关。WorkBuddy、Trae CN Work、Devin 和 CodeArts 使用 Go 进程内 adapter;CodeArts 直连华为云 snap-access 的 OpenAI 兼容网关,不启动 CLI。 - 控制台负责账号、密钥、设置、目录、日志、登录、导入和更新操作。 - 这是个人网关,不实现计费、Redis 槽位、多租户商业网关或公开暴露的管理端口。 @@ -75,6 +75,7 @@ cmd/server - Qoder 的 CLI / worker 兼容性版本固定在 `worker/src/compat.mjs`,不兼容时应明确失败。 - Qoder 聊天改为在 Go 进程内使用 COSY 客户端签名直连网关([`internal/providers/qoder/cosy.go`](../internal/providers/qoder/cosy.go))。COSY 协议版本固定在 [`COSYVersion = "1.1.32"`](cosy.go:23),支持中国区(`gateway.qoder.com.cn`)和全球区(`api1.qoder.sh`)双端点。 - WorkBuddy、Trae CN Work、Devin 使用进程内 adapter,不复制 Qoder worker 生命周期。 +- CodeArts 使用进程内 adapter:OAuth PKCE 只用于换取短期 STS 凭据,聊天直连 `snap-access.cn-north-4.myhuaweicloud.com` 的 OpenAI 兼容端点;不得改为每请求启动码道 CLI。 - provider 负责上游事实映射;executor 负责是否切号、冷却多久和是否 failover。 - Provider 能力、模型目录和 reasoning level 必须以实际 catalog 声明为准,不凭空增加模型能力。 diff --git a/frontend/src/api/overview.ts b/frontend/src/api/overview.ts index b77c4d97..ee8bcee6 100644 --- a/frontend/src/api/overview.ts +++ b/frontend/src/api/overview.ts @@ -132,7 +132,7 @@ export function updateProviderMaxMode(provider: string, modelKey: string, maxMod }) } -export function updateProviderReasoning(provider: 'trae' | 'workbuddy' | 'kimi', modelKey: string, reasoningEffort: string) { +export function updateProviderReasoning(provider: 'trae' | 'workbuddy' | 'kimi' | 'codearts', modelKey: string, reasoningEffort: string) { return api<{ model: string provider: string diff --git a/frontend/src/components/AddAccountModal.tsx b/frontend/src/components/AddAccountModal.tsx index cc11577b..70e9ee0d 100644 --- a/frontend/src/components/AddAccountModal.tsx +++ b/frontend/src/components/AddAccountModal.tsx @@ -67,6 +67,7 @@ const labelKeys: Record = { 'qwenwork-global': 'accountTypeQwenworkGlobal', 'qwenwork-cn': 'accountTypeQwenworkCN', 'kimi-cn': 'accountTypeKimiCN', + 'codearts-cn-north-4': 'accountTypeCodeArtsCN', 'meta-global': 'accountTypeMetaGlobal', } @@ -83,6 +84,7 @@ const hintKeys: Record = { 'qwenwork-global': 'accountTypeQwenworkGlobalHint', 'qwenwork-cn': 'accountTypeQwenworkCNHint', 'kimi-cn': 'accountTypeKimiCNHint', + 'codearts-cn-north-4': 'accountTypeCodeArtsCNHint', 'meta-global': 'accountTypeMetaGlobalHint', } @@ -200,7 +202,7 @@ export function AddAccountModal({ isOpen, onClose, onAdded }: Props) { // so the wizard opens on the PAT tab instead of the browser tab. const hasBrowserLogin = activeOption?.descriptor.capabilities?.browser_login !== false const showDropSystem = activeOption?.provider === 'workbuddy' || activeOption?.provider === 'trae' - const showCallbackPaste = activeOption?.provider === 'trae' || activeOption?.provider === 'devin' || activeOption?.provider === 'codex' + const showCallbackPaste = activeOption?.provider === 'trae' || activeOption?.provider === 'devin' || activeOption?.provider === 'codex' || activeOption?.provider === 'codearts' const busy = phase === 'busy' || phase === 'polling' const settingsLocked = Boolean(createdId.current) || busy const isDone = phase === 'done' @@ -234,11 +236,11 @@ export function AddAccountModal({ isOpen, onClose, onAdded }: Props) { max_inflight: parsedMaxInFlight(), priority: parsedPriority(), // WorkBuddy exposes the switch because upstream content screening rejects - // third-party system prompts. Trae and Kimi keep caller system prompts: - // clients such as MiMo Code put JSON instructions there, and those - // families have no equivalent screening. Every other provider keeps the - // historical drop=true default. - drop_system_prompt: showDropSystem ? dropSystemPrompt : activeOption?.provider !== 'kimi', + // third-party system prompts. Trae, Kimi and CodeArts keep caller system + // prompts: clients such as MiMo Code put JSON instructions there, and + // those families have no equivalent screening. Every other provider keeps + // the historical drop=true default. + drop_system_prompt: showDropSystem ? dropSystemPrompt : activeOption?.provider !== 'kimi' && activeOption?.provider !== 'codearts', proxy_url: proxyUrl.trim(), } } @@ -320,7 +322,7 @@ export function AddAccountModal({ isOpen, onClose, onAdded }: Props) { const option = providerOptions.find((item) => item.id === next) setAccountType(next) setTab(option?.provider === 'zhipu' ? 'pat' : 'browser') - setDropSystemPrompt(option?.provider !== 'trae' && option?.provider !== 'kimi') + setDropSystemPrompt(option?.provider !== 'trae' && option?.provider !== 'kimi' && option?.provider !== 'codearts') setMessage('') setStep('login') } diff --git a/frontend/src/i18n/messages.ts b/frontend/src/i18n/messages.ts index 46a907bf..ba80504c 100644 --- a/frontend/src/i18n/messages.ts +++ b/frontend/src/i18n/messages.ts @@ -13,7 +13,7 @@ export const messages: Record = { routesHint: 'OpenAI / Anthropic-compatible routes', clientConfig: 'Client configuration', clientConfigHint: 'Use the same console key as Bearer token', - providerPrefixHint: 'Pin a provider with a qoder/, workbuddy/, trae/, devin/, command/, codex/, zhipu/, qwenwork/, kimi/, or meta/ model prefix. A named API key can also limit which providers are listed and routed. Bare IDs follow the cross-provider pool setting on System.', + providerPrefixHint: 'Pin a provider with a qoder/, workbuddy/, trae/, devin/, command/, codex/, zhipu/, qwenwork/, kimi/, codearts/, or meta/ model prefix. A named API key can also limit which providers are listed and routed. Bare IDs follow the cross-provider pool setting on System.', authentication: 'Authentication', curlExample: 'curl example', quickTestHint: 'Send a real request through the selected account', @@ -806,6 +806,8 @@ export const messages: Record = { accountTypeQwenworkCNHint: '千问办公 China (qwenwork.cn) via browser OAuth or imported credential. The authorization link is single-use: open it directly in a browser already signed in to the target account, and do not send it through chat tools. The CN region has not yet been verified with a real account.', accountTypeKimiCN: 'Kimi Code CN', accountTypeKimiCNHint: 'Kimi Code China subscription via device-code sign-in against auth.kimi.com. Uses the same OAuth client as the official CLI, so the login you already completed with Kimi Code can be pasted in as its credential JSON.', + accountTypeCodeArtsCN: 'CodeArts (码道)', + accountTypeCodeArtsCNHint: 'Huawei Cloud CodeArts Agent via OAuth PKCE against codearts.huaweicloud.com. The console opens the authorization page; a pasted 127.0.0.1 callback URL also completes the login when the browser cannot reach this host.', accountTypeMetaGlobal: 'Muse (Meta)', accountTypeMetaGlobalHint: 'Meta Muse Code subscription via device-code sign-in against auth.meta.com. After approval the console mints an LLM API key at api.meta.ai. You can also paste a Meta Model API key on the PAT tab.', providerGroupSubscriptions: 'Subscriptions', @@ -965,7 +967,7 @@ export const messages: Record = { routesHint: 'OpenAI / Anthropic 兼容端点', clientConfig: '客户端配置', clientConfigHint: '使用与控制台相同的密钥作为 Bearer Token', - providerPrefixHint: '用 qoder/、workbuddy/、trae/、devin/、command/、codex/、zhipu/、qwenwork/、kimi/ 或 meta/ 前缀指定供应商。客户端 API 密钥也可以限制可见和可调度的供应商。不带前缀时是否跨供应商,由系统设置里的模型池决定。', + providerPrefixHint: '用 qoder/、workbuddy/、trae/、devin/、command/、codex/、zhipu/、qwenwork/、kimi/、codearts/ 或 meta/ 前缀指定供应商。客户端 API 密钥也可以限制可见和可调度的供应商。不带前缀时是否跨供应商,由系统设置里的模型池决定。', authentication: '认证方式', curlExample: 'curl 示例', quickTestHint: '通过所选账号发送一条真实请求', @@ -1758,6 +1760,8 @@ export const messages: Record = { accountTypeQwenworkCNHint: '千问办公国内版(qwenwork.cn),浏览器授权或导入凭证。授权链接只能使用一次:请在已登录目标账号的浏览器窗口里直接打开,不要经聊天工具发送。国内版尚未经真实账号验收。', accountTypeKimiCN: 'Kimi Code 国内版', accountTypeKimiCNHint: 'Kimi Code 中国区订阅,走 auth.kimi.com 的设备码授权,与官方 CLI 使用同一个 OAuth 客户端。已在本机登录 Kimi Code 的,可直接粘贴它的凭证 JSON 导入。', + accountTypeCodeArtsCN: 'CodeArts(码道)', + accountTypeCodeArtsCNHint: '华为云码道,走 codearts.huaweicloud.com 的 OAuth PKCE 授权。控制台打开授权页;浏览器回不到本机时,也可以粘贴 127.0.0.1 回调地址完成登录。', accountTypeMetaGlobal: 'Muse (Meta)', accountTypeMetaGlobalHint: 'Meta Muse Code 订阅,走 auth.meta.com 的设备码授权;批准后控制台会在 api.meta.ai 兑换 LLM API Key。也可在 PAT 页粘贴 Meta Model API Key。', providerGroupSubscriptions: '稳定订阅', diff --git a/frontend/src/lib/provider.ts b/frontend/src/lib/provider.ts index d20903ea..09ece430 100644 --- a/frontend/src/lib/provider.ts +++ b/frontend/src/lib/provider.ts @@ -31,6 +31,10 @@ export function isKimiProvider(provider?: string) { return String(provider || '').toLowerCase() === 'kimi' } +export function isCodeArtsProvider(provider?: string) { + return String(provider || '').toLowerCase() === 'codearts' +} + export function isMetaProvider(provider?: string) { const value = String(provider || '').toLowerCase() return value === 'meta' || value === 'muse' @@ -48,6 +52,7 @@ export function accountProviderFamilyLabel( if (isZhipuProvider(providerID)) return 'Zhipu GLM' if (isQwenworkProvider(providerID)) return 'QwenWork' if (isKimiProvider(providerID)) return 'Kimi Code' + if (isCodeArtsProvider(providerID)) return 'CodeArts' if (isMetaProvider(providerID)) return 'Muse (Meta)' if (isQoderProvider(providerID)) return 'Qoder' return provider || t('account') @@ -81,6 +86,9 @@ export function accountProviderLabel( if (isKimiProvider(providerID)) { return t('accountTypeKimiCN') } + if (isCodeArtsProvider(providerID)) { + return t('accountTypeCodeArtsCN') + } if (isMetaProvider(providerID)) { return t('accountTypeMetaGlobal') } diff --git a/frontend/src/pages/ProvidersPage.tsx b/frontend/src/pages/ProvidersPage.tsx index 062ca9f0..0bbd6f21 100644 --- a/frontend/src/pages/ProvidersPage.tsx +++ b/frontend/src/pages/ProvidersPage.tsx @@ -299,9 +299,9 @@ export function ProvidersPage() { async function onReasoningChange(model: ModelInfo, effort: string) { const provider = modelProvider(model) - // Providers with a settable reasoning default: Trae, WorkBuddy, and Kimi - // Code. Anything else has no console-saved level to write. - if (provider !== 'trae' && provider !== 'workbuddy' && provider !== 'kimi') return + // Providers with a settable reasoning default: Trae, WorkBuddy, Kimi + // Code, and CodeArts. Anything else has no console-saved level to write. + if (provider !== 'trae' && provider !== 'workbuddy' && provider !== 'kimi' && provider !== 'codearts') return const key = modelSettingsKey(model) setSavingKey(key) setMessage('') diff --git a/internal/accounts/policy.go b/internal/accounts/policy.go index 8def0264..ea83c942 100644 --- a/internal/accounts/policy.go +++ b/internal/accounts/policy.go @@ -29,15 +29,15 @@ func DefaultPriorityValue(value int) int { // DefaultDropSystemPrompt is the create-time default. WorkBuddy strips caller // system prompts because upstream content screening rejects third-party -// identity. Trae and Kimi keep them: clients such as MiMo Code put JSON -// instructions in the system prompt, and those families have no equivalent +// identity. Trae, Kimi, and CodeArts keep them: clients such as MiMo Code put +// JSON instructions in the system prompt, and those families have no equivalent // screening. An explicit value always wins. func DefaultDropSystemPrompt(provider string, value *bool) bool { if value != nil { return *value } switch strings.ToLower(strings.TrimSpace(provider)) { - case "trae", "kimi": + case "trae", "kimi", "codearts": return false default: return true diff --git a/internal/accounts/policy_test.go b/internal/accounts/policy_test.go index 371bd8ed..1f683a2e 100644 --- a/internal/accounts/policy_test.go +++ b/internal/accounts/policy_test.go @@ -21,6 +21,9 @@ func TestDefaultAccountPolicyValues(t *testing.T) { if DefaultDropSystemPrompt("kimi", nil) { t.Fatal("kimi must keep caller system prompts by default") } + if DefaultDropSystemPrompt("codearts", nil) { + t.Fatal("codearts must keep caller system prompts by default") + } off := false if DefaultDropSystemPrompt("workbuddy", &off) { t.Fatal("explicit drop system prompt false must stick") diff --git a/internal/app/app.go b/internal/app/app.go index e5c5747a..38ce3407 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -22,6 +22,7 @@ import ( apigateway "github.com/caigee-cmd/cli2api/internal/gateway" applogs "github.com/caigee-cmd/cli2api/internal/logs" "github.com/caigee-cmd/cli2api/internal/providers" + "github.com/caigee-cmd/cli2api/internal/providers/codearts" "github.com/caigee-cmd/cli2api/internal/providers/codex" "github.com/caigee-cmd/cli2api/internal/providers/command" "github.com/caigee-cmd/cli2api/internal/providers/devin" @@ -123,6 +124,7 @@ func New(cfg config.Config) *App { providerReg.Register(zhipu.NewClient(store).Adapter()) providerReg.Register(qwenwork.NewClient(store).Adapter()) providerReg.Register(kimi.NewClient(store).Adapter()) + providerReg.Register(codearts.NewClient(store).Adapter()) providerReg.Register(meta.NewClient(store).Adapter()) qoderClient := qoder.NewClient() qoderClient.Bind(manager.AccountURL, manager.ProxyAPIKey) diff --git a/internal/app/architecture_test.go b/internal/app/architecture_test.go index c6949578..969ed70e 100644 --- a/internal/app/architecture_test.go +++ b/internal/app/architecture_test.go @@ -173,7 +173,7 @@ func isConcreteProvider(imp string) bool { rest := strings.TrimPrefix(imp, prefix) root, _, _ := strings.Cut(rest, "/") switch root { - case "qoder", "workbuddy", "trae", "devin", "command", "codex", "zhipu", "qwenwork", "kimi", "meta": + case "qoder", "workbuddy", "trae", "devin", "command", "codex", "zhipu", "qwenwork", "kimi", "codearts", "meta": return true default: return false diff --git a/internal/console/models.go b/internal/console/models.go index 4b5ac8ed..13a1dab9 100644 --- a/internal/console/models.go +++ b/internal/console/models.go @@ -11,7 +11,7 @@ import ( func splitModelSettingPath(raw, queryProvider string) (provider, modelKey string) { raw = strings.TrimPrefix(raw, "/api/models/") provider = strings.ToLower(strings.TrimSpace(queryProvider)) - for _, prefix := range []string{"trae/", "workbuddy/", "qoder/", "devin/", "command/", "codex/", "zhipu/", "qwenwork/", "kimi/"} { + for _, prefix := range []string{"trae/", "workbuddy/", "qoder/", "devin/", "command/", "codex/", "zhipu/", "qwenwork/", "kimi/", "codearts/"} { if strings.HasPrefix(strings.ToLower(raw), prefix) { provider = strings.TrimSuffix(prefix, "/") raw = raw[len(prefix):] @@ -103,9 +103,9 @@ func modelSettingResponse(setting control.ModelSetting) map[string]any { "max_mode": setting.MaxMode, "reasoning_effort": setting.ReasoningEffort, "context_custom": setting.ContextCustom, } - case "kimi": - // Kimi Code has no max-mode tier, so only the reasoning default is - // part of its setting payload. + case "kimi", "codearts": + // Kimi Code and CodeArts have no max-mode tier, so only the reasoning + // default is part of their setting payload. return map[string]any{ "model": setting.Model, "provider": setting.Provider, "reasoning_effort": setting.ReasoningEffort, diff --git a/internal/console/models_test.go b/internal/console/models_test.go index 67baa9a8..fc601084 100644 --- a/internal/console/models_test.go +++ b/internal/console/models_test.go @@ -29,3 +29,24 @@ func TestModelSettingResponseKimiCarriesReasoningOnly(t *testing.T) { t.Fatal("kimi has no max-mode switch to report") } } + +func TestModelSettingResponseCodeArtsCarriesReasoningOnly(t *testing.T) { + response := modelSettingResponse(control.ModelSetting{ + Provider: "codearts", + Model: "glm-5.2", + ReasoningEffort: "max", + ContextCustom: true, + }) + if response["reasoning_effort"] != "max" { + t.Fatalf("reasoning_effort = %#v", response["reasoning_effort"]) + } + if response["context_custom"] != true { + t.Fatalf("context_custom = %#v", response["context_custom"]) + } + if response["provider"] != "codearts" || response["model"] != "glm-5.2" { + t.Fatalf("identity = %#v", response) + } + if _, ok := response["max_mode"]; ok { + t.Fatal("codearts has no max-mode switch to report") + } +} diff --git a/internal/control/catalog_policy.go b/internal/control/catalog_policy.go index 9b7cdc0b..448810a9 100644 --- a/internal/control/catalog_policy.go +++ b/internal/control/catalog_policy.go @@ -366,7 +366,7 @@ func DecorateModelsWithContext(ctx context.Context, settings *Settings, models [ item["catalog_context_length"] = catalogWindow } switch provider { - case "trae", "workbuddy", "kimi": + case "trae", "workbuddy", "kimi", "codearts": decorateProviderSettings(ctx, settings, item, provider, settingsKey) case "qoder": stored, hasStored := configured[settingsKey] diff --git a/internal/control/catalog_source_test.go b/internal/control/catalog_source_test.go index dd518874..2655a254 100644 --- a/internal/control/catalog_source_test.go +++ b/internal/control/catalog_source_test.go @@ -253,7 +253,7 @@ func TestDecorateModelsWithContextAppliesTraeMaxMode(t *testing.T) { } func TestDecorateProviderSettingsRestoresDefaultReasoningState(test *testing.T) { - for _, provider := range []string{"trae", "workbuddy", "kimi"} { + for _, provider := range []string{"trae", "workbuddy", "kimi", "codearts"} { test.Run(provider, func(test *testing.T) { services, _, _, _ := newTestServices() source := []map[string]any{{ diff --git a/internal/control/settings.go b/internal/control/settings.go index 38a0794f..7b789881 100644 --- a/internal/control/settings.go +++ b/internal/control/settings.go @@ -116,7 +116,7 @@ func (s *Settings) ReadModelSetting(ctx context.Context, provider, modelID strin provider = strings.ToLower(strings.TrimSpace(provider)) out := ModelSetting{Provider: provider, Model: modelID} switch provider { - case "trae", "workbuddy", "kimi": + case "trae", "workbuddy", "kimi", "codearts": setting, err := s.GetProviderModelSetting(ctx, provider, modelID) if err != nil { return ModelSetting{}, operationError("model_setting_failed", err.Error()) @@ -164,7 +164,7 @@ func (s *Settings) UpdateModelSetting(ctx context.Context, provider, modelID str modelID = ModelContextKey(modelID) provider = strings.ToLower(strings.TrimSpace(provider)) switch provider { - case "trae", "workbuddy", "kimi": + case "trae", "workbuddy", "kimi", "codearts": setting, err := s.UpdateProviderModelSetting(ctx, provider, modelID, patch) if err != nil { return ModelSetting{}, err @@ -214,6 +214,10 @@ func (s *Settings) UpdateProviderModelSetting(ctx context.Context, provider, mod if provider == "kimi" && patch.MaxMode != nil { return accounts.ProviderModelSetting{}, operationError("invalid_request", "kimi has no max-mode switch") } + // CodeArts exposes reasoning strengths, not a max-context toggle. + if provider == "codearts" && patch.MaxMode != nil { + return accounts.ProviderModelSetting{}, operationError("invalid_request", "codearts has no max-mode switch") + } setting, err := s.GetProviderModelSetting(ctx, provider, modelID) if err != nil { return accounts.ProviderModelSetting{}, operationError("model_setting_failed", err.Error()) diff --git a/internal/executor/prepare.go b/internal/executor/prepare.go index 5c76f591..049e1f6b 100644 --- a/internal/executor/prepare.go +++ b/internal/executor/prepare.go @@ -175,7 +175,7 @@ func (e ChatExecutor) Prepare(in PrepareInput) (PreparedRequest, error) { // "/" model id. Command Code's own model ids are org-namespaced // ("deepseek/…", "moonshotai/…"), so its provider prefix sits in front of an // already-namespaced id ("command/deepseek/…"). -var providerPrefixes = []string{"qoder/", "workbuddy/", "trae/", "devin/", "command/", "codex/", "zhipu/", "qwenwork/", "kimi/"} +var providerPrefixes = []string{"qoder/", "workbuddy/", "trae/", "devin/", "command/", "codex/", "zhipu/", "qwenwork/", "kimi/", "codearts/"} // regionDashSuffixes are the region qualifiers that may be appended to a // provider prefix with a dash: "workbuddy-cn/", "workbuddy-gl/", diff --git a/internal/executor/prepare_test.go b/internal/executor/prepare_test.go index 74ae52fb..a759565a 100644 --- a/internal/executor/prepare_test.go +++ b/internal/executor/prepare_test.go @@ -263,6 +263,7 @@ func TestProviderPrefixRecognizesCommand(t *testing.T) { "zhipu/glm-5.2": "zhipu", "command/deepseek/deepseek-v4-pro": "command", "command/moonshotai/Kimi-K3": "command", + "codearts/GLM-5.2": "codearts", // Region-qualified dash form. "workbuddy-cn/glm-5.3-flash": "workbuddy", "workbuddy-gl/glm-5.3-flash": "workbuddy", @@ -293,6 +294,7 @@ func TestResolveProviderFilterRegionPrefix(t *testing.T) { {"qoder-auto/glm-5.2", "qoder", "", "glm-5.2"}, {"workbuddy-auto/glm-5.3-flash", "workbuddy", "", "glm-5.3-flash"}, {"command/deepseek/deepseek-v4-pro", "command", "", "deepseek/deepseek-v4-pro"}, + {"codearts/GLM-5.2", "codearts", "", "GLM-5.2"}, } for _, tc := range cases { req := translate.ChatRequest{Model: tc.model} diff --git a/internal/providers/codearts/NOTICE b/internal/providers/codearts/NOTICE new file mode 100644 index 00000000..57333db7 --- /dev/null +++ b/internal/providers/codearts/NOTICE @@ -0,0 +1,26 @@ +This package adapts protocol facts and portions of the request/SSE mapping from +codearts2api (https://github.com/HITZY2002/codearts2api), commit +8d1e43d6f31279ac20cf94b92eff20bdba784b76, which is licensed under the MIT +License: + +MIT License + +Copyright (c) 2026 HITZY2002 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/internal/providers/codearts/availability.go b/internal/providers/codearts/availability.go new file mode 100644 index 00000000..ce639eae --- /dev/null +++ b/internal/providers/codearts/availability.go @@ -0,0 +1,85 @@ +package codearts + +import ( + "strings" + "time" + + "github.com/caigee-cmd/cli2api/internal/providers" +) + +// modelAvailabilityTTL is how long a learned per-account model verdict is kept. +// CodeArts catalogs list models an account cannot actually call, so the adapter +// learns from real chat traffic and filters the catalog after the first 404. +const modelAvailabilityTTL = 30 * time.Minute + +type modelAvailability struct { + unusable bool + at time.Time +} + +func availabilityKey(accountID, model string) string { + return accountID + "|" + strings.ToLower(strings.TrimSpace(model)) +} + +func (c *Client) markModelUnusable(accountID, model string) { + if accountID == "" || strings.TrimSpace(model) == "" { + return + } + c.availMu.Lock() + defer c.availMu.Unlock() + if c.availability == nil { + c.availability = map[string]modelAvailability{} + } + c.availability[availabilityKey(accountID, model)] = modelAvailability{unusable: true, at: c.now()} +} + +func (c *Client) markModelUsable(accountID, model string) { + if accountID == "" || strings.TrimSpace(model) == "" { + return + } + c.availMu.Lock() + defer c.availMu.Unlock() + if c.availability == nil { + c.availability = map[string]modelAvailability{} + } + c.availability[availabilityKey(accountID, model)] = modelAvailability{at: c.now()} +} + +func (c *Client) modelUnusable(accountID, model string) bool { + c.availMu.Lock() + defer c.availMu.Unlock() + entry, ok := c.availability[availabilityKey(accountID, model)] + if !ok || c.now().Sub(entry.at) > modelAvailabilityTTL { + return false + } + return entry.unusable +} + +// modelUnavailableBody recognizes the two upstream verdicts that mean "this +// account cannot call this model", as opposed to an auth or transport failure. +func modelUnavailableBody(body string) bool { + lower := strings.ToLower(body) + for _, marker := range []string{ + "inferhub.002002009", "not registered", "4004.200", "benefit not found", + "model is not available", "model not found", + } { + if strings.Contains(lower, marker) { + return true + } + } + return false +} + +// filterAvailable removes models the account has already been told it cannot +// call. The catalog itself is not rewritten so a later benefit claim can make +// the model visible again. +func (c *Client) filterAvailable(accountID string, models []providers.ModelInfo) []providers.ModelInfo { + out := make([]providers.ModelInfo, 0, len(models)) + for _, model := range models { + if c.modelUnusable(accountID, model.NativeModel) || c.modelUnusable(accountID, model.PublicModel) { + continue + } + out = append(out, model) + } + return out +} diff --git a/internal/providers/codearts/catalog.go b/internal/providers/codearts/catalog.go new file mode 100644 index 00000000..f1ebc144 --- /dev/null +++ b/internal/providers/codearts/catalog.go @@ -0,0 +1,602 @@ +package codearts + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "sort" + "strings" + + "github.com/caigee-cmd/cli2api/internal/providers" +) + +// modelRecord is one upstream model declaration. ContextWindow and MaxTokens +// are the only capability numbers the catalog publishes. +type modelRecord struct { + ID string + Name string + Description string + ContextWindow int64 + MaxTokens int64 + Images bool + Benefit bool + Reasoning []string +} + +// fetchModels merges the three official sources for one account. A source that +// fails is skipped; only an empty merge is an error. +func (c *Client) fetchModels(ctx context.Context, accountID string) ([]providers.ModelInfo, map[string]bool, error) { + credential, err := c.credential(ctx, accountID) + if err != nil { + return nil, nil, err + } + merged := map[string]modelRecord{} + benefit := map[string]bool{} + if records, fetchErr := c.fetchAgentModels(ctx, accountID, credential); fetchErr == nil { + mergeRecords(merged, records) + } + if records, fetchErr := c.fetchBuiltinModels(ctx, accountID, credential); fetchErr == nil { + mergeRecords(merged, records) + } + // Benefit discovery is additive. A failed claim/config (the normal state + // for credit-only trial accounts) must not hide agent models: the same + // model works over the credit path without maas_type: benefit. + if records, fetchErr := c.fetchBenefitModels(ctx, accountID, credential); fetchErr == nil { + for _, record := range records { + record.Benefit = true + benefit[strings.ToLower(record.ID)] = true + mergeRecords(merged, []modelRecord{record}) + } + } + if len(merged) == 0 { + return nil, nil, fmt.Errorf("codearts model discovery returned no models") + } + records := make([]modelRecord, 0, len(merged)) + for _, record := range merged { + records = append(records, record) + } + ids := make([]string, 0, len(records)) + for _, record := range records { + ids = append(ids, record.ID) + } + if variants, err := c.fetchThinkingVariants(ctx, accountID, credential, ids); err == nil { + for index := range records { + if options := variants[strings.ToLower(records[index].ID)]; len(options) > 0 { + records[index].Reasoning = options + } + } + } + sort.Slice(records, func(i, j int) bool { + left, right := strings.ToLower(records[i].ID), strings.ToLower(records[j].ID) + if left != right { + return left < right + } + return records[i].ID < records[j].ID + }) + models := make([]providers.ModelInfo, 0, len(records)) + for _, record := range records { + models = append(models, toModelInfo(record)) + } + return models, benefit, nil +} + +// fetchThinkingVariants asks the PromptCenter endpoint for the reasoning +// strengths each model accepts. The official provider uses the same endpoint +// to populate its model picker; without it the adapter has no declared levels +// and cannot distinguish a max request from "use the upstream default". +func (c *Client) fetchThinkingVariants(ctx context.Context, accountID string, credential Credential, modelIDs []string) (map[string][]string, error) { + if len(modelIDs) == 0 { + return nil, nil + } + body, err := json.Marshal(map[string]any{"model_names": modelIDs}) + if err != nil { + return nil, err + } + requestCtx, cancel := context.WithTimeout(ctx, metadataTimeout) + defer cancel() + req, err := http.NewRequestWithContext(requestCtx, http.MethodPost, c.apiBase(credential)+thinkingVariants, bytes.NewReader(body)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("Agent-Type", "PromptCenter") + req.Header.Set("X-Language", Language) + signRequest(req, body, credential.Sign()) + client, err := c.httpClient(ctx, accountID) + if err != nil { + return nil, err + } + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if resp.StatusCode >= 400 { + return nil, newProviderError(resp.StatusCode, string(raw)) + } + type variant struct { + Strengths []string `json:"strengths"` + } + var out struct { + Variants map[string]variant `json:"variants"` + Data struct { + Variants map[string]variant `json:"variants"` + } `json:"data"` + } + if err := json.Unmarshal(raw, &out); err != nil { + return nil, fmt.Errorf("parse codearts thinking variants: %w", err) + } + if len(out.Variants) == 0 { + out.Variants = out.Data.Variants + } + variants := make(map[string][]string, len(out.Variants)) + for model, value := range out.Variants { + options := normalizeReasoningOptions(value.Strengths) + if len(options) > 0 { + variants[strings.ToLower(strings.TrimSpace(model))] = options + } + } + return variants, nil +} + +func normalizeReasoningOptions(strengths []string) []string { + seen := map[string]bool{} + options := make([]string, 0, len(strengths)) + for _, strength := range strengths { + level := providers.NormalizeReasoningLevel(strength) + if level == "" || seen[level] { + continue + } + seen[level] = true + options = append(options, level) + } + return options +} + +func defaultReasoningOption(options []string) string { + for _, option := range options { + if option != "none" { + return option + } + } + if len(options) > 0 { + return options[0] + } + return "" +} + +func mergeRecords(target map[string]modelRecord, records []modelRecord) { + for _, record := range records { + id := strings.TrimSpace(record.ID) + if id == "" { + continue + } + key := strings.ToLower(id) + if current, ok := target[key]; ok { + if current.ContextWindow == 0 { + current.ContextWindow = record.ContextWindow + } + if current.MaxTokens == 0 { + current.MaxTokens = record.MaxTokens + } + if current.Name == "" { + current.Name = record.Name + } + if current.Description == "" { + current.Description = record.Description + } + if len(current.Reasoning) == 0 { + current.Reasoning = record.Reasoning + } + current.Images = current.Images || record.Images + current.Benefit = current.Benefit || record.Benefit + target[key] = current + continue + } + target[key] = record + } +} + +// toModelInfo declares only what the upstream catalog publishes. Tools are a +// provider-level capability (the chat endpoint accepts them natively); images +// and reasoning levels are forwarded only when the catalog advertises them. +func toModelInfo(record modelRecord) providers.ModelInfo { + maxOutput := record.MaxTokens + if maxOutput > 65536 { + // The upstream rejects max_tokens above 65536 with + // InferHub.001001005 even when the catalog advertises more. + maxOutput = 65536 + } + caps := providers.ModelCapabilities{ + ContextWindow: int(record.ContextWindow), + ContextWindowMax: int(record.ContextWindow), + MaxOutput: int(maxOutput), + Tools: true, + Reasoning: true, + Images: record.Images, + } + if options := normalizeReasoningOptions(record.Reasoning); len(options) > 0 { + caps.ReasoningOptions = options + caps.ReasoningDefault = defaultReasoningOption(options) + for _, option := range options { + if option == "none" { + caps.CanDisableThinking = true + break + } + } + } + return providers.ModelInfo{ + NativeModel: record.ID, + PublicModel: record.ID, + DisplayName: firstNonEmpty(record.Name, record.ID), + Capabilities: caps, + } +} + +// Models implements providers.ModelCatalogProvider. +func (c *Client) Models(ctx context.Context, accountID string) ([]providers.ModelInfo, error) { + if models, _, ok := c.cachedCatalog(accountID); ok { + return cloneModels(c.filterAvailable(accountID, models)), nil + } + models, benefit, err := c.fetchModels(ctx, accountID) + if err != nil { + return nil, err + } + c.rememberCatalog(accountID, models, benefit) + return cloneModels(c.filterAvailable(accountID, models)), nil +} + +func (c *Client) cachedCatalog(accountID string) ([]providers.ModelInfo, map[string]bool, bool) { + c.mu.Lock() + defer c.mu.Unlock() + entry, ok := c.catalogs[accountID] + if !ok || len(entry.models) == 0 || c.now().Sub(entry.at) > catalogTTL { + return nil, nil, false + } + return cloneModels(entry.models), cloneBoolMap(entry.benefit), true +} + +func (c *Client) rememberCatalog(accountID string, models []providers.ModelInfo, benefit map[string]bool) { + c.mu.Lock() + defer c.mu.Unlock() + if c.catalogs == nil { + c.catalogs = map[string]catalogCache{} + } + c.catalogs[accountID] = catalogCache{models: cloneModels(models), benefit: cloneBoolMap(benefit), at: c.now()} +} + +func cloneModels(in []providers.ModelInfo) []providers.ModelInfo { + if len(in) == 0 { + return nil + } + out := make([]providers.ModelInfo, len(in)) + for i, model := range in { + out[i] = model + if len(model.Capabilities.ReasoningOptions) > 0 { + out[i].Capabilities.ReasoningOptions = append([]string(nil), model.Capabilities.ReasoningOptions...) + } + } + return out +} + +func cloneBoolMap(in map[string]bool) map[string]bool { + if len(in) == 0 { + return nil + } + out := make(map[string]bool, len(in)) + for key, value := range in { + out[key] = value + } + return out +} + +// canonicalModel returns the exact catalog spelling for a client-supplied id. +func (c *Client) canonicalModel(accountID, model string) string { + want := strings.ToLower(strings.TrimSpace(model)) + if want == "" { + return "" + } + models, _, ok := c.cachedCatalog(accountID) + if !ok { + return model + } + for _, info := range models { + if strings.ToLower(info.NativeModel) == want || strings.ToLower(info.PublicModel) == want { + return info.NativeModel + } + } + return model +} + +// isBenefitModel reports whether the account's catalog routes this model +// through the limited-benefit gateway. +func (c *Client) isBenefitModel(accountID, model string) bool { + _, benefit, ok := c.cachedCatalog(accountID) + if !ok { + return false + } + return benefit[strings.ToLower(strings.TrimSpace(model))] +} + +// fetchAgentModels reads the CodeAgent detail's gpts.models. +func (c *Client) fetchAgentModels(ctx context.Context, accountID string, credential Credential) ([]modelRecord, error) { + agentID, err := c.defaultAgentID(ctx, accountID, credential) + if err != nil || agentID == "" { + return nil, err + } + query := url.Values{} + query.Set("agent_id", agentID) + raw, err := c.signedGet(ctx, accountID, c.apiBase(credential)+agentDetailPath+"?"+query.Encode(), credential, true) + if err != nil { + return nil, err + } + var detail struct { + Gpts struct { + Models []struct { + ModelAlias string `json:"model_alias"` + ModelName string `json:"model_name"` + ModelID string `json:"model_id"` + SupportsImages bool `json:"supports_images"` + Params struct { + ContextWindow int64 `json:"context_window"` + MaxTokens int64 `json:"max_tokens"` + ModelID string `json:"model_id"` + ModelDesc string `json:"model_desc"` + SupportsImages bool `json:"supports_images"` + } `json:"model_parameters"` + } `json:"models"` + } `json:"gpts"` + } + if err := json.Unmarshal(raw, &detail); err != nil { + return nil, fmt.Errorf("parse codearts agent models: %w", err) + } + out := make([]modelRecord, 0, len(detail.Gpts.Models)) + for _, model := range detail.Gpts.Models { + // The chat endpoint registers the lowercase model_id, not the display + // name. The official client keys its provider catalog by model_id and + // uses model_name only as the label; using model_name here makes + // display names such as GLM-5.2-ArkTS-SPARK fail with 002002009 while + // the real id (glm-5.2-sft-harmony) is callable. + id := firstNonEmpty(model.Params.ModelID, model.ModelID, model.ModelAlias, model.ModelName) + if id == "" { + continue + } + out = append(out, modelRecord{ + ID: id, Name: firstNonEmpty(model.ModelName, id), Description: model.Params.ModelDesc, + ContextWindow: model.Params.ContextWindow, MaxTokens: model.Params.MaxTokens, + Images: model.SupportsImages || model.Params.SupportsImages, + }) + } + return out, nil +} + +func (c *Client) defaultAgentID(ctx context.Context, accountID string, credential Credential) (string, error) { + raw, err := c.signedGet(ctx, accountID, c.apiBase(credential)+agentListPath+"?offset=0&limit=100&is_primary_agent=true", credential, true) + if err != nil { + return "", err + } + var out struct { + Agents []struct { + AgentID string `json:"agent_id"` + AgentName string `json:"agent_name"` + Primary bool `json:"is_primary_agent"` + ShowInIDE bool `json:"show_in_ide"` + Alias struct { + ZhCN string `json:"alias_zh_cn"` + } `json:"alias"` + } `json:"agents"` + } + if err := json.Unmarshal(raw, &out); err != nil { + return "", fmt.Errorf("parse codearts agents: %w", err) + } + for _, agent := range out.Agents { + if agent.AgentName == "CodeAgent" && agent.Alias.ZhCN == "智能体" && agent.ShowInIDE { + return agent.AgentID, nil + } + } + for _, agent := range out.Agents { + if agent.Primary { + return agent.AgentID, nil + } + } + if len(out.Agents) > 0 { + return out.Agents[0].AgentID, nil + } + return "", nil +} + +// fetchBuiltinModels reads GET /v1/model/builtin. +func (c *Client) fetchBuiltinModels(ctx context.Context, accountID string, credential Credential) ([]modelRecord, error) { + requestCtx, cancel := context.WithTimeout(ctx, metadataTimeout) + defer cancel() + req, err := http.NewRequestWithContext(requestCtx, http.MethodGet, c.apiBase(credential)+modelBuiltinPath, nil) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("Agent-Type", "PromptCenter") + req.Header.Set("X-Language", Language) + signRequest(req, nil, credential.Sign()) + client, err := c.httpClient(ctx, accountID) + if err != nil { + return nil, err + } + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + raw, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<20)) + if resp.StatusCode >= 400 { + return nil, newProviderError(resp.StatusCode, string(raw)) + } + var out struct { + BuiltinModels []struct { + ModelID string `json:"model_id"` + ModelName string `json:"model_name"` + ModelDesc string `json:"model_desc"` + ContextWindow int64 `json:"context_window"` + MaxTokens int64 `json:"max_tokens"` + SupportsImages bool `json:"supports_images"` + } `json:"builtinModels"` + } + if err := json.Unmarshal(raw, &out); err != nil { + return nil, fmt.Errorf("parse codearts builtin models: %w", err) + } + records := make([]modelRecord, 0, len(out.BuiltinModels)) + for _, model := range out.BuiltinModels { + id := firstNonEmpty(model.ModelID, model.ModelName) + if id == "" { + continue + } + records = append(records, modelRecord{ + ID: id, Name: firstNonEmpty(model.ModelName, id), Description: model.ModelDesc, + ContextWindow: model.ContextWindow, MaxTokens: model.MaxTokens, + Images: model.SupportsImages, + }) + } + return records, nil +} + +// fetchBenefitModels reads the limited-benefit gateway config and, matching the +// official client opening the model menu, claims the benefit idempotently. +func (c *Client) fetchBenefitModels(ctx context.Context, accountID string, credential Credential) ([]modelRecord, error) { + if err := c.claimBenefit(ctx, accountID, credential); err != nil { + // Match the official client: do not load benefit models when claim + // failed. The same model may still be callable over the credit path, + // and fetchModels keeps the agent/builtin records for that case. + return nil, err + } + requestCtx, cancel := context.WithTimeout(ctx, metadataTimeout) + defer cancel() + req, err := http.NewRequestWithContext(requestCtx, http.MethodGet, c.benefitBase+benefitConfig, nil) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("X-Language", Language) + signRequest(req, nil, credential.Sign()) + client, err := c.httpClient(ctx, accountID) + if err != nil { + return nil, err + } + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + raw, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<20)) + if resp.StatusCode >= 400 { + return nil, newProviderError(resp.StatusCode, string(raw)) + } + var out struct { + ErrorCode string `json:"error_code"` + Result struct { + Models []struct { + ModelID string `json:"model_id"` + ModelName string `json:"model_name"` + ModelDesc string `json:"model_desc"` + ContextWindow int64 `json:"context_window"` + MaxTokens int64 `json:"max_tokens"` + SupportsImages bool `json:"supports_images"` + } `json:"models"` + } `json:"result"` + } + if err := json.Unmarshal(raw, &out); err != nil { + return nil, fmt.Errorf("parse codearts benefit config: %w", err) + } + if out.ErrorCode != "" && out.ErrorCode != "0000" { + return nil, fmt.Errorf("codearts benefit config error %s", out.ErrorCode) + } + records := make([]modelRecord, 0, len(out.Result.Models)) + for _, model := range out.Result.Models { + id := firstNonEmpty(model.ModelID, model.ModelName) + if id == "" { + continue + } + records = append(records, modelRecord{ + ID: id, Name: firstNonEmpty(model.ModelName, id), Description: model.ModelDesc, + ContextWindow: model.ContextWindow, MaxTokens: model.MaxTokens, + Images: model.SupportsImages, Benefit: true, + }) + } + return records, nil +} + +func (c *Client) claimBenefit(ctx context.Context, accountID string, credential Credential) error { + body := []byte("{}") + requestCtx, cancel := context.WithTimeout(ctx, metadataTimeout) + defer cancel() + req, err := http.NewRequestWithContext(requestCtx, http.MethodPost, c.benefitBase+benefitClaim, bytes.NewReader(body)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("Agent-Type", "PromptCenter") + req.Header.Set("X-Language", Language) + signRequest(req, body, credential.Sign()) + client, err := c.httpClient(ctx, accountID) + if err != nil { + return err + } + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if resp.StatusCode >= 400 { + return newProviderError(resp.StatusCode, string(raw)) + } + var envelope struct { + ErrorCode string `json:"error_code"` + ErrorMsg string `json:"error_msg"` + } + if err := json.Unmarshal(raw, &envelope); err != nil { + return fmt.Errorf("parse codearts benefit claim: %w", err) + } + if envelope.ErrorCode != "" && envelope.ErrorCode != "0000" { + return fmt.Errorf("codearts benefit claim error %s: %s", envelope.ErrorCode, envelope.ErrorMsg) + } + return nil +} + +// signedGet sends a signed GET and returns the body. +func (c *Client) signedGet(ctx context.Context, accountID, endpoint string, credential Credential, agentCenter bool) ([]byte, error) { + requestCtx, cancel := context.WithTimeout(ctx, metadataTimeout) + defer cancel() + req, err := http.NewRequestWithContext(requestCtx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("X-Language", Language) + if agentCenter { + req.Header.Set("Agent-Type", "AgentCenter") + } + signRequest(req, nil, credential.Sign()) + client, err := c.httpClient(ctx, accountID) + if err != nil { + return nil, err + } + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + raw, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<20)) + if resp.StatusCode >= 400 { + return nil, newProviderError(resp.StatusCode, string(raw)) + } + return raw, nil +} diff --git a/internal/providers/codearts/chat.go b/internal/providers/codearts/chat.go new file mode 100644 index 00000000..a5d6dba6 --- /dev/null +++ b/internal/providers/codearts/chat.go @@ -0,0 +1,313 @@ +package codearts + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "strings" + "time" + + "github.com/caigee-cmd/cli2api/internal/accounts" + "github.com/caigee-cmd/cli2api/internal/providers" + "github.com/caigee-cmd/cli2api/internal/translate" +) + +// ChatStream posts the mapped request and returns a normalized +// chat-completions SSE body. The upstream is OpenAI-shaped, but the adapter +// normalizes event names and appends a final usage chunk. +func (c *Client) ChatStream(ctx context.Context, accountID string, req translate.ChatRequest) (*http.Response, providers.ResolvedChat, error) { + upstream, level, err := c.doChat(ctx, accountID, req) + if err != nil { + return nil, providers.ResolvedChat{}, err + } + reader, err := preflightWithTimeout(upstream) + if err != nil { + if providerErr, ok := err.(*providers.Error); ok && providerErr.Kind == accounts.KindModelNotAvailable { + c.markModelUnusable(accountID, req.Model) + } + return nil, providers.ResolvedChat{}, err + } + pr, pw := io.Pipe() + model := c.canonicalModel(accountID, req.Model) + go func() { + defer upstream.Body.Close() + defer pw.Close() + if err := streamCapture(pw, reader, model); err != nil { + if providerErr, ok := err.(*providers.Error); ok && providerErr.Kind == accounts.KindModelNotAvailable { + c.markModelUnusable(accountID, model) + } + _ = pw.CloseWithError(err) + } + }() + return &http.Response{ + StatusCode: http.StatusOK, + Status: "200 OK", + Header: http.Header{"Content-Type": []string{"text/event-stream"}, "Cache-Control": []string{"no-cache"}}, + Body: pr, + Request: upstream.Request, + }, providers.ResolvedChat{ReasoningLevel: level}, nil +} + +// ChatNonStream forces a stream and aggregates it, keeping usage, reasoning and +// tool-call assembly on the same parser as the streaming path. +func (c *Client) ChatNonStream(ctx context.Context, accountID string, req translate.ChatRequest) (providers.ChatOutcome, error) { + upstream, level, err := c.doChat(ctx, accountID, req) + if err != nil { + return providers.ChatOutcome{}, err + } + defer upstream.Body.Close() + completion, err := aggregateRaw(upstream.Body) + if err != nil { + if providerErr, ok := err.(*providers.Error); ok && providerErr.Kind == accounts.KindModelNotAvailable { + c.markModelUnusable(accountID, req.Model) + } + return providers.ChatOutcome{}, err + } + outcome := providers.ChatOutcome{ + Model: c.canonicalModel(accountID, req.Model), + Content: completion.Content, + Reasoning: completion.Reasoning, + FinishReason: completion.Finish, + UsageSource: "upstream", + ReasoningLevel: level, + } + if len(completion.ToolCalls) > 0 { + outcome.FinishReason = "tool_calls" + raw, err := json.Marshal(completion.ToolCalls) + if err != nil { + return providers.ChatOutcome{}, err + } + outcome.ToolCalls = raw + } + if completion.Usage != nil { + outcome.PromptTokens = int(numberFrom(completion.Usage, "prompt_tokens")) + outcome.CompletionTokens = int(numberFrom(completion.Usage, "completion_tokens")) + if details, ok := completion.Usage["prompt_tokens_details"].(map[string]any); ok { + if cached := numberFrom(details, "cached_tokens"); cached > 0 { + value := int(cached) + outcome.CacheReadTokens = &value + } + } + } else { + outcome.UsageSource = "" + } + if outcome.FinishReason == "" { + outcome.FinishReason = "stop" + } + return outcome, nil +} + +func (c *Client) doChat(ctx context.Context, accountID string, req translate.ChatRequest) (*http.Response, string, error) { + credential, err := c.credential(ctx, accountID) + if err != nil { + return nil, "", err + } + if _, _, ok := c.cachedCatalog(accountID); !ok { + // The benefit header and exact model spelling depend on the account + // catalog. A failed discovery is not fatal: chat still carries the + // client's model id and the upstream will answer with a clear error. + _, _ = c.Models(ctx, accountID) + } + model := strings.TrimSpace(req.Model) + model = strings.TrimPrefix(model, "codearts/") + if model == "" || model == "auto" { + return nil, "", &providers.Error{ + Kind: accounts.KindInvalidRequest, + Status: http.StatusBadRequest, + Message: "codearts model is required", + } + } + model = c.canonicalModel(accountID, model) + level, caps := c.resolveLevel(ctx, accountID, model, requestedLevel(req)) + body, err := buildChatBody(req, model, level, caps.Images, c.isBenefitModel(accountID, model)) + if err != nil { + return nil, "", err + } + raw, err := json.Marshal(body) + if err != nil { + return nil, "", err + } + resp, err := c.sendChat(ctx, accountID, credential, raw, model, c.isBenefitModel(accountID, model)) + if err != nil { + return nil, "", err + } + if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden { + payload, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + resp.Body.Close() + if Classify(resp.StatusCode, string(payload)).Kind == accounts.KindModelNotAvailable { + c.markModelUnusable(accountID, model) + return nil, "", newProviderError(resp.StatusCode, string(payload)) + } + refreshed, refreshErr := c.forceRefresh(ctx, accountID) + if refreshErr != nil { + return nil, "", refreshErr + } + resp, err = c.sendChat(ctx, accountID, refreshed, raw, model, c.isBenefitModel(accountID, model)) + if err != nil { + return nil, "", err + } + } + if resp.StatusCode >= 300 { + payload, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + resp.Body.Close() + if modelUnavailableBody(string(payload)) { + c.markModelUnusable(accountID, model) + } + return nil, "", newProviderError(resp.StatusCode, string(payload)) + } + c.markModelUsable(accountID, model) + return resp, level, nil +} + +// sendChat performs the signed request and enforces the TTFB deadline. +func (c *Client) sendChat(ctx context.Context, accountID string, credential Credential, body []byte, model string, benefit bool) (*http.Response, error) { + base := c.apiBase(credential) + requestCtx, cancel := context.WithCancel(ctx) + req, err := http.NewRequestWithContext(requestCtx, http.MethodPost, base+chatPath, bytes.NewReader(body)) + if err != nil { + cancel() + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "text/event-stream") + req.Header.Set("x-auth-token", credential.SecurityToken) + req.Header.Set("x-snap-traceid", randomHex(16)) + req.Header.Set("X-Language", Language) + req.Header.Set("app-id", AppID) + req.Header.Set("is_confidential", "false") + if benefit { + req.Header.Set(HeaderMaasType, MaasBenefit) + } + signRequest(req, body, credential.Sign()) + if id, ok := payloadChatID(body); ok && id != "" { + req.Header.Set("Chat-Id", id) + req.Header.Set("Session-Id", id) + } + client, err := c.httpClient(ctx, accountID) + if err != nil { + cancel() + return nil, err + } + type result struct { + resp *http.Response + err error + } + done := make(chan result, 1) + go func() { + resp, err := client.Do(req) + done <- result{resp: resp, err: err} + }() + select { + case <-ctx.Done(): + cancel() + return nil, ctx.Err() + case <-time.After(firstByteTimeout): + cancel() + return nil, providers.ErrFirstByteTimeout + case outcome := <-done: + if outcome.err != nil { + cancel() + return nil, outcome.err + } + outcome.resp.Body = &cancelReadCloser{ReadCloser: outcome.resp.Body, cancel: cancel} + return outcome.resp, nil + } +} + +type cancelReadCloser struct { + io.ReadCloser + cancel context.CancelFunc +} + +func (c *cancelReadCloser) Close() error { + err := c.ReadCloser.Close() + c.cancel() + return err +} + +func payloadChatID(body []byte) (string, bool) { + var probe struct { + ChatID string `json:"chat_id"` + } + if err := json.Unmarshal(body, &probe); err != nil { + return "", false + } + return probe.ChatID, probe.ChatID != "" +} + +// resolveLevel applies the catalog clamp. CodeArts does not publish a per-model +// effort list, so a model with no options sends no reasoning_effort and relies +// on the upstream default. +func (c *Client) resolveLevel(ctx context.Context, accountID, model, requested string) (string, providers.ModelCapabilities) { + caps := c.modelCapabilities(accountID, model) + level := requested + if level == "" { + level = c.storedReasoning(ctx, model) + } + return providers.ResolveReasoningLevel(level, caps), caps +} + +func (c *Client) modelCapabilities(accountID, model string) providers.ModelCapabilities { + if models, _, ok := c.cachedCatalog(accountID); ok { + for _, info := range models { + if strings.EqualFold(info.NativeModel, model) || strings.EqualFold(info.PublicModel, model) { + return info.Capabilities + } + } + } + return providers.ModelCapabilities{} +} + +func requestedLevel(req translate.ChatRequest) string { + if len(req.ReasoningEffort) == 0 { + return "" + } + var value any + if json.Unmarshal(req.ReasoningEffort, &value) != nil { + return "" + } + switch typed := value.(type) { + case string: + return providers.NormalizeReasoningLevel(typed) + case map[string]any: + for _, key := range []string{"effort", "level", "type"} { + if text, ok := typed[key].(string); ok { + return providers.NormalizeReasoningLevel(text) + } + } + } + return "" +} + +// ModelSettingReader is optional. Missing it means the console reasoning +// default is skipped and the catalog default is used. +type ModelSettingReader interface { + GetProviderModelSetting(context.Context, string, string) (accounts.ProviderModelSetting, error) +} + +func (c *Client) storedReasoning(ctx context.Context, model string) string { + reader, ok := c.store.(ModelSettingReader) + if !ok { + return "" + } + stored, err := reader.GetProviderModelSetting(ctx, ProviderID, accounts.CanonicalModelID(model)) + if err != nil { + return "" + } + return strings.TrimSpace(stored.ReasoningEffort) +} + +func numberFrom(values map[string]any, key string) float64 { + switch value := values[key].(type) { + case float64: + return value + case json.Number: + parsed, _ := value.Float64() + return parsed + case int: + return float64(value) + } + return 0 +} diff --git a/internal/providers/codearts/checkin.go b/internal/providers/codearts/checkin.go new file mode 100644 index 00000000..56daa74e --- /dev/null +++ b/internal/providers/codearts/checkin.go @@ -0,0 +1,222 @@ +package codearts + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/caigee-cmd/cli2api/internal/accounts" + "github.com/caigee-cmd/cli2api/internal/providers" +) + +const creditChannel = "IDE" + +type creditCampaign struct { + ID int `json:"campaignId"` + Title string `json:"title"` + Status string `json:"status"` + Claimable bool `json:"claimable"` + BenefitUnit string `json:"benefitUnit"` + BenefitAmount float64 `json:"benefitAmount"` + PendingTotalAmount float64 `json:"pendingTotalAmount"` + Extra struct { + StartTime string `json:"startTime"` + EndTime string `json:"endTime"` + } `json:"extra"` +} + +type creditClaim struct { + CampaignID int + Title string + OK bool + Status string + Credits float64 + Message string +} + +// Checkin claims the daily check-in (campaign 1) and the new-user campaign +// (campaign 4). The upstream decides eligibility and the daily reset; already +// claimed campaigns only get their missing confirmation. +func (c *Client) Checkin(ctx context.Context, accountID string) (providers.CheckinResult, error) { + credential, err := c.credential(ctx, accountID) + if err != nil { + return providers.CheckinResult{}, err + } + campaigns, err := c.fetchCampaigns(ctx, accountID, credential) + if err != nil { + return providers.CheckinResult{}, err + } + var claims []creditClaim + total := 0.0 + allOK := true + for _, campaign := range campaigns { + if campaign.ID != 1 && campaign.ID != 4 { + continue + } + if campaign.BenefitUnit != "" && campaign.BenefitUnit != "CREDIT" { + continue + } + claim := c.claimCampaign(ctx, accountID, credential, campaign) + claims = append(claims, claim) + allOK = allOK && claim.OK + total += claim.Credits + } + if len(claims) == 0 { + return providers.CheckinResult{Status: "skipped", Message: "no check-in or new-user campaign"}, nil + } + status := "success" + if !allOK { + status = "skipped" + } else { + already := true + for _, claim := range claims { + if claim.Credits > 0 { + already = false + } + } + if already { + status = "already" + } + } + parts := make([]string, 0, len(claims)) + for _, claim := range claims { + parts = append(parts, claim.Title+": "+claim.Message) + } + return providers.CheckinResult{ + Status: status, + Message: strings.Join(parts, "; "), + RewardCredits: total, + }, nil +} + +func (c *Client) claimCampaign(ctx context.Context, accountID string, credential Credential, campaign creditCampaign) creditClaim { + claim := creditClaim{CampaignID: campaign.ID, Title: campaign.Title, Status: campaign.Status, OK: true} + if claim.Title == "" { + claim.Title = fmt.Sprintf("credit campaign %d", campaign.ID) + } + now := c.now() + if end, err := time.Parse(time.RFC3339Nano, campaign.Extra.EndTime); err == nil && !now.Before(end) { + claim.Message = "campaign ended" + return claim + } + if start, err := time.Parse(time.RFC3339Nano, campaign.Extra.StartTime); err == nil && now.Before(start) { + claim.Message = "campaign has not started" + return claim + } + switch campaign.Status { + case "CONFIRMED", "CONSUMED": + claim.Message = "already claimed" + return claim + case "CLAIMED": + // Already delivered; only the confirm step may be missing. + case "ELIGIBLE": + if !campaign.Claimable { + claim.OK = false + claim.Message = "not claimable yet" + return claim + } + if _, err := c.creditRequest(ctx, accountID, credential, http.MethodPost, creditClaimPath, map[string]any{ + "campaignId": campaign.ID, "channel": creditChannel, + "idempotentKey": fmt.Sprintf("claim_%d_%d", campaign.ID, now.UnixMilli()), + }); err != nil { + claim.OK = false + claim.Message = err.Error() + return claim + } + claim.Status = "CLAIMED" + claim.Credits = campaign.PendingTotalAmount + if claim.Credits <= 0 { + claim.Credits = campaign.BenefitAmount + } + case "": + claim.Message = "not eligible" + return claim + default: + claim.OK = false + claim.Message = "unknown campaign status: " + campaign.Status + return claim + } + if _, err := c.creditRequest(ctx, accountID, credential, http.MethodPost, creditConfirmPath, map[string]any{"campaignId": campaign.ID}); err != nil { + claim.OK = false + claim.Message = "claimed but confirmation failed: " + err.Error() + return claim + } + claim.Status = "CONFIRMED" + claim.Message = "confirmed" + if claim.Credits > 0 { + claim.Message = fmt.Sprintf("claimed %g credits", claim.Credits) + } + return claim +} + +func (c *Client) fetchCampaigns(ctx context.Context, accountID string, credential Credential) ([]creditCampaign, error) { + raw, err := c.creditRequest(ctx, accountID, credential, http.MethodGet, creditDeliveryPath+"?channel="+creditChannel, nil) + if err != nil { + return nil, err + } + var data struct { + Items *[]creditCampaign `json:"items"` + } + if err := json.Unmarshal(raw, &data); err != nil { + return nil, fmt.Errorf("parse codearts credit campaigns: %w", err) + } + if data.Items == nil { + return nil, fmt.Errorf("codearts credit response missing data.items") + } + return *data.Items, nil +} + +func (c *Client) creditRequest(ctx context.Context, accountID string, credential Credential, method, path string, body any) (json.RawMessage, error) { + var raw []byte + var err error + if body != nil { + raw, err = json.Marshal(body) + if err != nil { + return nil, err + } + } + requestCtx, cancel := context.WithTimeout(ctx, metadataTimeout) + defer cancel() + req, err := http.NewRequestWithContext(requestCtx, method, c.apiBase(credential)+path, bytes.NewReader(raw)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("Agent-Type", "PromptCenter") + req.Header.Set("X-Language", Language) + signRequest(req, raw, credential.Sign()) + client, err := c.httpClient(ctx, accountID) + if err != nil { + return nil, err + } + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + response, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if resp.StatusCode >= 400 { + return nil, newProviderError(resp.StatusCode, string(response)) + } + var envelope struct { + Code *int `json:"code"` + Message string `json:"message"` + Data json.RawMessage `json:"data"` + } + if err := json.Unmarshal(response, &envelope); err != nil { + return nil, fmt.Errorf("parse codearts credit response: %w", err) + } + if envelope.Code == nil { + return nil, fmt.Errorf("codearts credit response missing business code") + } + if *envelope.Code != 0 { + return nil, &providers.Error{Kind: accounts.KindInvalidRequest, Status: http.StatusBadRequest, Message: envelope.Message} + } + return envelope.Data, nil +} diff --git a/internal/providers/codearts/client.go b/internal/providers/codearts/client.go new file mode 100644 index 00000000..d627f101 --- /dev/null +++ b/internal/providers/codearts/client.go @@ -0,0 +1,489 @@ +package codearts + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "sync" + "time" + + "github.com/caigee-cmd/cli2api/internal/accounts" + "github.com/caigee-cmd/cli2api/internal/providers" + proxyutil "github.com/caigee-cmd/cli2api/internal/proxy" +) + +// Store is the persistence surface this adapter needs. It never imports +// internal/store. +type Store interface { + Get(ctx context.Context, id string) (accounts.Account, error) + LoadCredentialPayload(ctx context.Context, accountID string) (string, []byte, error) + SaveCredentialPayload(ctx context.Context, accountID, format string, payload []byte) error + Observe(ctx context.Context, id, remoteUID, status, lastError, lastKind string) error +} + +// SecretReader is optional. Missing it means no global proxy. +type SecretReader interface { + GetSecret(context.Context, string) (string, bool, error) +} + +type catalogCache struct { + models []providers.ModelInfo + benefit map[string]bool + at time.Time +} + +// Client is the in-process CodeArts adapter. One client serves every account; +// credentials, catalogs and login rounds stay keyed by account id. +type Client struct { + store Store + http *http.Client + + transports proxyutil.TransportCache + nowFn func() time.Time + + baseURL string + stsHost string + portalHost string + benefitBase string + + mu sync.Mutex + availMu sync.Mutex + catalogs map[string]catalogCache + locks map[string]*sync.Mutex + logins map[string]*loginState + // availability is a learned per-account model verdict from real traffic. + availability map[string]modelAvailability +} + +func NewClient(store Store) *Client { + transport := &http.Transport{ + MaxIdleConns: 20, + MaxIdleConnsPerHost: 4, + IdleConnTimeout: 90 * time.Second, + } + return &Client{ + store: store, + http: &http.Client{ + Transport: transport, + CheckRedirect: func(*http.Request, []*http.Request) error { + return http.ErrUseLastResponse + }, + }, + baseURL: CNBaseURL, + stsHost: CNSTSHost, + portalHost: CNPortalHost, + benefitBase: BenefitHost, + catalogs: map[string]catalogCache{}, + locks: map[string]*sync.Mutex{}, + logins: map[string]*loginState{}, + availability: map[string]modelAvailability{}, + nowFn: time.Now, + } +} + +func (c *Client) now() time.Time { + if c.nowFn != nil { + return c.nowFn() + } + return time.Now() +} + +func (c *Client) apiBase(credential Credential) string { + if base := credential.AuthBase(); base != "" { + return base + } + if c.baseURL != "" { + return c.baseURL + } + return CNBaseURL +} + +func (c *Client) globalProxy(ctx context.Context) (string, error) { + store, ok := c.store.(SecretReader) + if !ok { + return "", nil + } + value, found, err := store.GetSecret(ctx, "proxy_url") + if err != nil { + return "", fmt.Errorf("load global proxy setting: %w", err) + } + if !found { + return "", nil + } + return strings.TrimSpace(value), nil +} + +func (c *Client) effectiveProxy(ctx context.Context, accountID string) (string, error) { + if strings.TrimSpace(accountID) == "" { + return c.globalProxy(ctx) + } + account, err := c.store.Get(ctx, accountID) + if err != nil { + return "", err + } + if value := strings.TrimSpace(account.ProxyURL); value != "" { + return value, nil + } + return c.globalProxy(ctx) +} + +func (c *Client) httpClient(ctx context.Context, accountID string) (*http.Client, error) { + rawProxy, err := c.effectiveProxy(ctx, accountID) + if err != nil { + return nil, err + } + client := *c.http + transport, err := c.transports.Get(rawProxy) + if err != nil { + return nil, err + } + if transport != nil { + client.Transport = transport + } + return &client, nil +} + +// accountLock serializes refresh per account so a burst cannot burn the +// single-use refresh token more than once. +func (c *Client) accountLock(accountID string) *sync.Mutex { + c.mu.Lock() + defer c.mu.Unlock() + if c.locks == nil { + c.locks = map[string]*sync.Mutex{} + } + lock, ok := c.locks[accountID] + if !ok { + lock = &sync.Mutex{} + c.locks[accountID] = lock + } + return lock +} + +func (c *Client) loadCredential(ctx context.Context, accountID string) (Credential, error) { + _, payload, err := c.store.LoadCredentialPayload(ctx, accountID) + if err != nil { + return Credential{}, err + } + return DecodeCredential(payload) +} + +func (c *Client) saveCredential(ctx context.Context, accountID string, credential Credential) error { + encoded, err := credential.Encode() + if err != nil { + return err + } + return c.store.SaveCredentialPayload(ctx, accountID, CredentialFormat, encoded) +} + +// credential returns a credential that can sign a request now, refreshing and +// persisting a rotated STS trio when the stored one is near expiry. +func (c *Client) credential(ctx context.Context, accountID string) (Credential, error) { + credential, err := c.loadCredential(ctx, accountID) + if err != nil { + return Credential{}, err + } + if !credential.Ready() && !credential.CanRefresh() { + return credential, &providers.Error{ + Kind: accounts.KindAuth, + Status: http.StatusUnauthorized, + Message: "codearts account has no STS credential; sign in again", + } + } + if !credential.NeedsRefresh(c.now()) { + return credential, nil + } + lock := c.accountLock(accountID) + lock.Lock() + defer lock.Unlock() + current, err := c.loadCredential(ctx, accountID) + if err != nil { + return Credential{}, err + } + if !current.NeedsRefresh(c.now()) { + return current, nil + } + refreshed, err := c.refreshCredential(ctx, current) + if err != nil { + return Credential{}, err + } + if err := c.persistRefreshed(ctx, accountID, refreshed); err != nil { + return Credential{}, err + } + return refreshed, nil +} + +// persistRefreshed stores a rotated credential. The refresh token is +// single-use, so a failed write must fail the request instead of continuing +// with a credential only this process knows about. +func (c *Client) persistRefreshed(ctx context.Context, accountID string, credential Credential) error { + var err error + for attempt := 0; attempt < 2; attempt++ { + if err = c.saveCredential(ctx, accountID, credential); err == nil { + return nil + } + } + return &providers.Error{ + Kind: accounts.KindUnavailable, + Status: http.StatusBadGateway, + Message: "codearts credential refresh succeeded but could not be saved: " + err.Error(), + } +} + +func (c *Client) forceRefresh(ctx context.Context, accountID string) (Credential, error) { + lock := c.accountLock(accountID) + lock.Lock() + defer lock.Unlock() + current, err := c.loadCredential(ctx, accountID) + if err != nil { + return Credential{}, err + } + refreshed, err := c.refreshCredential(ctx, current) + if err != nil { + return Credential{}, err + } + if err := c.persistRefreshed(ctx, accountID, refreshed); err != nil { + return Credential{}, err + } + return refreshed, nil +} + +// Probe verifies the current credential against the signed identity endpoint. +func (c *Client) Probe(ctx context.Context, accountID string) (providers.AccountHealth, error) { + credential, err := c.credential(ctx, accountID) + if err != nil { + health := providers.AccountHealth{LastError: err.Error()} + if providerErr, ok := err.(*providers.Error); ok { + health.LastError = providerErr.Message + } + return health, nil + } + uid, name, domain, err := c.currentUser(ctx, credential) + if err != nil { + health := providers.AccountHealth{LastError: err.Error()} + if providerErr, ok := err.(*providers.Error); ok { + health.LastError = providerErr.Message + } + return health, nil + } + changed := false + if uid != "" && credential.UserID != uid { + credential.UserID = uid + changed = true + } + if name != "" && credential.UserName != name { + credential.UserName = name + changed = true + } + if domain != "" && credential.DomainID != domain { + credential.DomainID = domain + changed = true + } + if changed { + _ = c.saveCredential(ctx, accountID, credential) + } + _ = c.store.Observe(ctx, accountID, uid, "ready", "", "") + return providers.AccountHealth{Ready: true, Hot: true, UID: uid}, nil +} + +// Adapter wires the capability surface. +func (c *Client) Adapter() providers.Adapter { + return providers.Adapter{ + ID: ProviderID, + Credential: credentialCodec{}, + Login: c, + Chat: c, + Models: c, + Classifier: classifier{}, + ImportExport: importer{}, + Prober: c, + Checkin: c, + } +} + +type credentialCodec struct{} + +func (credentialCodec) Validate(payload []byte) error { return ValidateCredential(payload) } + +// tokenResponse is the STS oauth2/tokens response. +type tokenResponse struct { + UserID string `json:"user_id"` + UserName string `json:"user_name"` + DomainID string `json:"domain_id"` + RefreshToken string `json:"refresh_token"` + Credentials credentials `json:"credentials"` + Credential legacyCred `json:"credential"` + Error string `json:"error"` + ErrorCode string `json:"error_code"` + ErrorMessage string `json:"error_msg"` +} + +type credentials struct { + AccessKeyID string `json:"access_key_id"` + SecretAccessKey string `json:"secret_access_key"` + SecurityToken string `json:"security_token"` + Expiration string `json:"expiration"` +} + +type legacyCred struct { + Access string `json:"access"` + Secret string `json:"secret"` + SecurityToken string `json:"securitytoken"` + ExpiresAt string `json:"expires_at"` +} + +// refreshExpiredError marks a terminal refresh rejection; retrying cannot help. +type refreshExpiredError struct { + status int + message string +} + +func (e *refreshExpiredError) Error() string { return e.message } + +// requestToken posts a form to the STS token endpoint with a DPoP proof. +func (c *Client) requestToken(ctx context.Context, form map[string][]string, cred Credential) (*tokenResponse, error) { + endpoint := c.stsHost + oauthTokensPath + kp, err := dpopKeyPairFor(cred.DPoPPrivateKey) + if err != nil { + return nil, err + } + proof, err := signDPoPProof(kp, endpoint) + if err != nil { + return nil, fmt.Errorf("dpop proof: %w", err) + } + values := make(map[string]string, len(form)) + for k, v := range form { + if len(v) > 0 { + values[k] = v[0] + } + } + // Drop members that were not part of the grant instead of sending empty + // values; STS rejects an empty code or redirect_uri on refresh. + cleaned := url.Values{} + for _, key := range []string{"client_id", "code", "code_verifier", "grant_type", "redirect_uri", "refresh_token"} { + if value := strings.TrimSpace(values[key]); value != "" { + cleaned.Set(key, value) + } + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(cleaned.Encode())) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("DPoP", proof) + client, err := c.httpClient(ctx, "") + if err != nil { + return nil, err + } + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + raw, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<20)) + if resp.StatusCode >= 400 { + var tokenErr tokenResponse + _ = json.Unmarshal(raw, &tokenErr) + grantType := "" + if values := form["grant_type"]; len(values) > 0 { + grantType = values[0] + } + if grantType == "refresh_token" { + code := tokenErr.ErrorCode + message := strings.ToLower(tokenErr.ErrorMessage) + if tokenErr.Error == "invalid_grant" || strings.Contains(code, "ExpiredRefreshToken") || + strings.Contains(code, "InvalidDPoPHeader") || strings.Contains(code, "invalid client id") || + strings.Contains(message, "has been used") { + return nil, &refreshExpiredError{ + status: resp.StatusCode, + message: fmt.Sprintf("codearts refresh token rejected: http=%d code=%s msg=%s", + resp.StatusCode, code, truncate(tokenErr.ErrorMessage, 160)), + } + } + } + return nil, newProviderError(resp.StatusCode, string(raw)) + } + var out tokenResponse + if err := json.Unmarshal(raw, &out); err != nil { + return nil, fmt.Errorf("parse codearts token response: %w", err) + } + if out.Credentials.SecurityToken == "" && out.Credential.SecurityToken != "" { + out.Credentials = credentials{ + AccessKeyID: out.Credential.Access, + SecretAccessKey: out.Credential.Secret, + SecurityToken: out.Credential.SecurityToken, + Expiration: out.Credential.ExpiresAt, + } + } + if out.Credentials.SecurityToken == "" { + return nil, fmt.Errorf("codearts token response missing credentials") + } + return &out, nil +} + +// refreshCredential rotates the STS trio with the stored refresh token, PKCE +// verifier and DPoP key. +func (c *Client) refreshCredential(ctx context.Context, credential Credential) (Credential, error) { + if !credential.CanRefresh() { + return Credential{}, &providers.Error{ + Kind: accounts.KindAuth, + Status: http.StatusUnauthorized, + Message: "codearts credential is expired and cannot be refreshed; sign in again", + } + } + clientID := credential.ClientID + if clientID == "" { + clientID = ClientID + } + token, err := c.requestToken(ctx, map[string][]string{ + "client_id": {clientID}, + "code_verifier": {credential.CodeVerifier}, + "grant_type": {"refresh_token"}, + "refresh_token": {credential.RefreshToken}, + }, credential) + if err != nil { + if expired, ok := err.(*refreshExpiredError); ok { + return Credential{}, &providers.Error{ + Kind: accounts.KindAuth, + Status: http.StatusUnauthorized, + Message: expired.message, + } + } + return Credential{}, err + } + return applyToken(credential, token), nil +} + +func applyToken(credential Credential, token *tokenResponse) Credential { + credential.Format = CredentialFormat + credential.AccessKeyID = token.Credentials.AccessKeyID + credential.SecretAccessKey = token.Credentials.SecretAccessKey + credential.SecurityToken = token.Credentials.SecurityToken + if token.Credentials.Expiration != "" { + credential.ExpiresAt = token.Credentials.Expiration + } + if token.UserID != "" { + credential.UserID = token.UserID + } + if token.UserName != "" { + credential.UserName = token.UserName + } + if token.DomainID != "" { + credential.DomainID = token.DomainID + } + if token.RefreshToken != "" { + credential.RefreshToken = token.RefreshToken + } + return credential +} + +func truncate(s string, n int) string { + s = strings.TrimSpace(s) + if len(s) > n { + return s[:n] + } + return s +} diff --git a/internal/providers/codearts/codearts_test.go b/internal/providers/codearts/codearts_test.go new file mode 100644 index 00000000..7b793d00 --- /dev/null +++ b/internal/providers/codearts/codearts_test.go @@ -0,0 +1,679 @@ +package codearts + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "sync" + "testing" + "time" + + "github.com/caigee-cmd/cli2api/internal/accounts" + "github.com/caigee-cmd/cli2api/internal/providers" + "github.com/caigee-cmd/cli2api/internal/translate" +) + +func writeSSE(t *testing.T, w io.Writer, payload any) { + t.Helper() + raw, err := json.Marshal(payload) + if err != nil { + t.Fatal(err) + } + if _, err := fmt.Fprintf(w, "data: %s\n\n", raw); err != nil { + t.Fatal(err) + } +} + +type memoryStore struct { + mu sync.Mutex + payload []byte + account accounts.Account + setting accounts.ProviderModelSetting + saved []byte +} + +func (s *memoryStore) Get(context.Context, string) (accounts.Account, error) { + return s.account, nil +} + +func (s *memoryStore) LoadCredentialPayload(context.Context, string) (string, []byte, error) { + s.mu.Lock() + defer s.mu.Unlock() + return CredentialFormat, append([]byte(nil), s.payload...), nil +} + +func (s *memoryStore) SaveCredentialPayload(_ context.Context, _ string, _ string, payload []byte) error { + s.mu.Lock() + defer s.mu.Unlock() + s.payload = append([]byte(nil), payload...) + s.saved = append([]byte(nil), payload...) + return nil +} + +func (s *memoryStore) Observe(context.Context, string, string, string, string, string) error { + return nil +} + +func (s *memoryStore) GetProviderModelSetting(context.Context, string, string) (accounts.ProviderModelSetting, error) { + s.mu.Lock() + defer s.mu.Unlock() + return s.setting, nil +} + +func testCredential() Credential { + return Credential{ + Format: CredentialFormat, + AccessKeyID: "AK", + SecretAccessKey: "SK", + SecurityToken: "STS", + ExpiresAt: time.Now().Add(24 * time.Hour).Format(time.RFC3339Nano), + UserID: "user-1", + UserName: "tester", + DomainID: "domain-1", + } +} + +func newTestClient(t *testing.T, server *httptest.Server) (*Client, *memoryStore) { + t.Helper() + credential := testCredential() + payload, err := credential.Encode() + if err != nil { + t.Fatal(err) + } + store := &memoryStore{ + payload: payload, + account: accounts.Account{ID: "acct-1", Provider: ProviderID, ProviderRegion: "cn-north-4", ProxyURL: ""}, + } + client := NewClient(store) + client.baseURL = server.URL + client.benefitBase = server.URL + client.stsHost = server.URL + client.portalHost = server.URL + client.nowFn = func() time.Time { return time.Now() } + return client, store +} + +func TestValidateCredential(t *testing.T) { + valid, _ := testCredential().Encode() + if err := ValidateCredential(valid); err != nil { + t.Fatalf("valid credential rejected: %v", err) + } + if err := ValidateCredential([]byte(`{"access_key_id":"AK"}`)); err == nil { + t.Fatal("credential without secrets accepted") + } + if err := ValidateCredential([]byte(`{"refresh_token":"r","code_verifier":"v"}`)); err != nil { + t.Fatalf("refresh-only credential rejected: %v", err) + } +} + +func TestChatNonStreamAndToolMapping(t *testing.T) { + var gotBody map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case chatPath: + raw, _ := io.ReadAll(r.Body) + _ = json.Unmarshal(raw, &gotBody) + if r.Header.Get("x-auth-token") != "STS" || r.Header.Get("Authorization") == "" { + t.Errorf("missing auth headers: %+v", r.Header) + } + w.Header().Set("Content-Type", "text/event-stream") + writeSSE(t, w, map[string]any{"choices": []any{map[string]any{"index": 0, "delta": map[string]any{"role": "assistant"}}}}) + writeSSE(t, w, map[string]any{"choices": []any{map[string]any{"index": 0, "delta": map[string]any{"reasoning_content": "think"}}}}) + writeSSE(t, w, map[string]any{"choices": []any{map[string]any{"index": 0, "delta": map[string]any{"content": "hello"}}}}) + writeSSE(t, w, map[string]any{"choices": []any{map[string]any{"index": 0, "delta": map[string]any{"tool_calls": []any{map[string]any{ + "index": 0, "id": "call_1", "type": "function", + "function": map[string]any{"name": "lookup", "arguments": `{"q":"x"}`}, + }}}}}}) + writeSSE(t, w, map[string]any{"choices": []any{map[string]any{"index": 0, "delta": map[string]any{}, "finish_reason": "tool_calls"}}}) + writeSSE(t, w, map[string]any{"choices": []any{}, "usage": map[string]any{"prompt_tokens": 5, "completion_tokens": 3, "total_tokens": 8}}) + _, _ = io.WriteString(w, "data: [DONE]\n\n") + default: + http.NotFound(w, r) + } + })) + defer server.Close() + client, _ := newTestClient(t, server) + + tools := json.RawMessage(`[{"type":"function","function":{"name":"lookup","description":"look up","parameters":{"type":"object","properties":{"q":{"type":"string"}}}}}]`) + req := translate.ChatRequest{ + Model: "GLM-5.2", + Messages: []translate.ChatMessage{{Role: "user", Content: "hi"}}, + Tools: tools, + ToolChoice: json.RawMessage(`{"type":"function","function":{"name":"lookup"}}`), + } + outcome, err := client.ChatNonStream(context.Background(), "acct-1", req) + if err != nil { + t.Fatalf("ChatNonStream: %v", err) + } + if outcome.Content != "hello" || outcome.Reasoning != "think" { + t.Fatalf("content/reasoning = %q/%q", outcome.Content, outcome.Reasoning) + } + if outcome.FinishReason != "tool_calls" || len(outcome.ToolCalls) == 0 { + t.Fatalf("tool outcome = %+v", outcome) + } + if outcome.PromptTokens != 5 || outcome.CompletionTokens != 3 { + t.Fatalf("usage = %d/%d", outcome.PromptTokens, outcome.CompletionTokens) + } + if gotBody["tool_choice"] != "required" { + t.Fatalf("tool_choice = %v", gotBody["tool_choice"]) + } + if gotBody["model"] != "GLM-5.2" { + t.Fatalf("model = %v", gotBody["model"]) + } +} + +func TestChatForwardsImagePartsForVisionModel(t *testing.T) { + var gotBody map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != chatPath { + http.NotFound(w, r) + return + } + raw, _ := io.ReadAll(r.Body) + _ = json.Unmarshal(raw, &gotBody) + w.Header().Set("Content-Type", "text/event-stream") + writeSSE(t, w, map[string]any{"choices": []any{map[string]any{"index": 0, "delta": map[string]any{"content": "seen"}}}}) + _, _ = io.WriteString(w, "data: [DONE]\n\n") + })) + defer server.Close() + client, _ := newTestClient(t, server) + client.rememberCatalog("acct-1", []providers.ModelInfo{{ + NativeModel: "vision-model", PublicModel: "vision-model", + Capabilities: providers.ModelCapabilities{Images: true}, + }}, nil) + + _, err := client.ChatNonStream(context.Background(), "acct-1", translate.ChatRequest{ + Model: "vision-model", + Messages: []translate.ChatMessage{{ + Role: "user", + Content: []any{ + map[string]any{"type": "text", "text": "describe"}, + map[string]any{"type": "image_url", "image_url": map[string]any{"url": "data:image/png;base64,AAAA"}}, + }, + }}, + }) + if err != nil { + t.Fatalf("ChatNonStream: %v", err) + } + messages, _ := gotBody["messages"].([]any) + if len(messages) == 0 { + t.Fatalf("messages = %#v", gotBody["messages"]) + } + first, _ := messages[0].(map[string]any) + parts, ok := first["content"].([]any) + if !ok || len(parts) != 2 { + t.Fatalf("image content parts = %#v", first["content"]) + } + image, _ := parts[1].(map[string]any) + imageValue, _ := image["image_url"].(map[string]any) + if image["type"] != "image_url" || imageValue["url"] != "data:image/png;base64,AAAA" { + t.Fatalf("image part = %#v", image) + } +} + +func TestChatRejectsImageForTextModel(t *testing.T) { + client, _ := newTestClient(t, httptest.NewServer(http.NotFoundHandler())) + client.rememberCatalog("acct-1", []providers.ModelInfo{{ + NativeModel: "text-model", PublicModel: "text-model", + Capabilities: providers.ModelCapabilities{Images: false}, + }}, nil) + _, err := client.ChatNonStream(context.Background(), "acct-1", translate.ChatRequest{ + Model: "text-model", + Messages: []translate.ChatMessage{{ + Role: "user", + Content: []any{map[string]any{ + "type": "image_url", "image_url": map[string]any{"url": "data:image/png;base64,AAAA"}, + }}, + }}, + }) + if err == nil || !strings.Contains(err.Error(), "does not support image input") { + t.Fatalf("error = %v", err) + } +} + +func TestModelsAdvertiseImageSupport(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case modelBuiltinPath: + _, _ = io.WriteString(w, `{"builtinModels":[{"model_id":"vision-model","supports_images":true,"context_window":1000,"max_tokens":100},{"model_id":"text-model","supports_images":false,"context_window":1000,"max_tokens":100}]}`) + case agentListPath, agentDetailPath: + http.NotFound(w, r) + case benefitClaim: + _, _ = io.WriteString(w, `{"error_code":"0000"}`) + case benefitConfig: + _, _ = io.WriteString(w, `{"error_code":"0000","result":{"models":[]}}`) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + client, _ := newTestClient(t, server) + models, err := client.Models(context.Background(), "acct-1") + if err != nil { + t.Fatal(err) + } + byID := map[string]providers.ModelInfo{} + for _, model := range models { + byID[model.NativeModel] = model + } + if !byID["vision-model"].Capabilities.Images { + t.Fatalf("vision model images = %+v", byID["vision-model"].Capabilities) + } + if byID["text-model"].Capabilities.Images { + t.Fatalf("text model images = %+v", byID["text-model"].Capabilities) + } +} + +func TestModelsDeclareThinkingVariantLevels(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case modelBuiltinPath: + _, _ = io.WriteString(w, `{"builtinModels":[{"model_id":"GLM-5.2","context_window":202752,"max_tokens":65536}]}`) + case agentListPath, agentDetailPath: + http.NotFound(w, r) + case benefitClaim: + _, _ = io.WriteString(w, `{"error_code":"0000"}`) + case benefitConfig: + _, _ = io.WriteString(w, `{"error_code":"0000","result":{"models":[]}}`) + case thinkingVariants: + _, _ = io.WriteString(w, `{"data":{"variants":{"GLM-5.2":{"strengths":["high","max"]}}}}`) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + client, _ := newTestClient(t, server) + models, err := client.Models(context.Background(), "acct-1") + if err != nil { + t.Fatal(err) + } + if len(models) != 1 { + t.Fatalf("models = %+v", models) + } + options := models[0].Capabilities.ReasoningOptions + if len(options) != 2 || options[0] != "high" || options[1] != "max" { + t.Fatalf("reasoning options = %+v", options) + } + if models[0].Capabilities.ReasoningDefault != "high" { + t.Fatalf("reasoning default = %q", models[0].Capabilities.ReasoningDefault) + } +} + +func TestResolveMaxReasoningHonorsRequestedLevel(t *testing.T) { + client := NewClient(&memoryStore{payload: mustTestCredential(t)}) + client.rememberCatalog("acct-1", []providers.ModelInfo{{ + NativeModel: "GLM-5.2", PublicModel: "GLM-5.2", + Capabilities: providers.ModelCapabilities{ + ReasoningOptions: []string{"high", "max"}, ReasoningDefault: "high", + }, + }}, nil) + level, caps := client.resolveLevel(context.Background(), "acct-1", "GLM-5.2", "max") + if level != "max" { + t.Fatalf("resolved level = %q", level) + } + body, err := buildChatBody(translate.ChatRequest{ + Model: "GLM-5.2", Messages: []translate.ChatMessage{{Role: "user", Content: "hi"}}, + }, "GLM-5.2", level, caps.Images, false) + if err != nil { + t.Fatal(err) + } + if body["reasoning_effort"] != "max" { + t.Fatalf("body reasoning_effort = %#v", body["reasoning_effort"]) + } +} + +func TestResolveStoredMaxReasoningHonorsConsoleSetting(t *testing.T) { + store := &memoryStore{ + payload: mustTestCredential(t), + setting: accounts.ProviderModelSetting{ReasoningEffort: "max"}, + } + client := NewClient(store) + client.rememberCatalog("acct-1", []providers.ModelInfo{{ + NativeModel: "GLM-5.2", PublicModel: "GLM-5.2", + Capabilities: providers.ModelCapabilities{ + ReasoningOptions: []string{"high", "max"}, ReasoningDefault: "high", + }, + }}, nil) + level, _ := client.resolveLevel(context.Background(), "acct-1", "GLM-5.2", "") + if level != "max" { + t.Fatalf("stored max resolved to %q", level) + } +} + +func mustTestCredential(t *testing.T) []byte { + t.Helper() + payload, err := testCredential().Encode() + if err != nil { + t.Fatal(err) + } + return payload +} + +func TestImageURLFromPartVariants(t *testing.T) { + cases := []struct { + name string + block map[string]any + want string + }{ + {"object", map[string]any{"type": "image_url", "image_url": map[string]any{"url": "https://example.com/a.png"}}, "https://example.com/a.png"}, + {"string map", map[string]any{"type": "image_url", "image_url": map[string]string{"url": "https://example.com/b.png"}}, "https://example.com/b.png"}, + {"string", map[string]any{"type": "image_url", "image_url": "https://example.com/c.png"}, "https://example.com/c.png"}, + {"anthropic source", map[string]any{"type": "image", "source": map[string]any{"data": "AAAA", "media_type": "image/png"}}, "data:image/png;base64,AAAA"}, + } + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + if got := imageURLFromPart(test.block); got != test.want { + t.Fatalf("url = %q, want %q", got, test.want) + } + }) + } +} + +func TestChatStreamEmitsDeltasAndDone(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != chatPath { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "text/event-stream") + writeSSE(t, w, map[string]any{"choices": []any{map[string]any{"index": 0, "delta": map[string]any{"content": "one"}}}}) + writeSSE(t, w, map[string]any{"choices": []any{map[string]any{"index": 0, "delta": map[string]any{"content": " two"}, "finish_reason": "stop"}}}) + writeSSE(t, w, map[string]any{"choices": []any{}, "usage": map[string]any{"prompt_tokens": 1, "completion_tokens": 2}}) + _, _ = io.WriteString(w, "data: [DONE]\n\n") + })) + defer server.Close() + client, _ := newTestClient(t, server) + resp, resolved, err := client.ChatStream(context.Background(), "acct-1", translate.ChatRequest{ + Model: "GLM-5.2", + Messages: []translate.ChatMessage{{Role: "user", Content: "hi"}}, + }) + if err != nil { + t.Fatalf("ChatStream: %v", err) + } + defer resp.Body.Close() + raw, _ := io.ReadAll(resp.Body) + body := string(raw) + if !strings.Contains(body, `"content":"one"`) || !strings.Contains(body, `"content":" two"`) { + t.Fatalf("stream body missing content: %s", body) + } + if !strings.Contains(body, `"usage":{`) || !strings.Contains(body, "data: [DONE]") { + t.Fatalf("stream body missing usage/done: %s", body) + } + if resolved.ReasoningLevel != "" { + t.Fatalf("unexpected reasoning level %q", resolved.ReasoningLevel) + } +} + +func TestModelsAndQuota(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v1/model/builtin": + _, _ = io.WriteString(w, `{"builtinModels":[{"model_id":"GLM-5.2","model_name":"GLM-5.2","context_window":200000,"max_tokens":131072}]}`) + case "/v1/agent-center/agents/useragents": + http.NotFound(w, r) + case "/api/v1/benefit/claim": + _, _ = io.WriteString(w, `{"error_code":"0000"}`) + case "/api/v1/gateway/config": + _, _ = io.WriteString(w, `{"error_code":"0000","result":{"models":[{"model_id":"glm-5.3-flash","context_window":1048576,"max_tokens":393216}]}}`) + case statisticsPath: + _, _ = io.WriteString(w, `{"end_date":"2026-12-31","package":{"spec_code":"free","package_name_en":"Free"},"metrics":[{"name":"usageTotalPackageCredit","show":true,"package_credit_amount":500,"package_credit_used":100,"package_credit_remain":400,"value":20},{"name":"usageTokenChatMessages","show":true,"usage_token_num":1000,"package_token_amount":10000,"value":10}]}`) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + client, _ := newTestClient(t, server) + + models, err := client.Models(context.Background(), "acct-1") + if err != nil { + t.Fatalf("Models: %v", err) + } + if len(models) != 2 { + t.Fatalf("models = %+v", models) + } + found := false + for _, model := range models { + if model.NativeModel == "GLM-5.2" { + found = true + if model.Capabilities.ContextWindow != 200000 || model.Capabilities.MaxOutput != 65536 { + t.Fatalf("capabilities = %+v", model.Capabilities) + } + } + } + if !found { + t.Fatal("GLM-5.2 missing") + } + if !client.isBenefitModel("acct-1", "glm-5.3-flash") { + t.Fatal("benefit model not marked") + } + quota, err := client.Quota(context.Background(), "acct-1") + if err != nil { + t.Fatalf("Quota: %v", err) + } + if quota.Total != 500 || quota.Remaining != 400 || quota.Unit != "积分" { + t.Fatalf("quota = %+v", quota) + } + if len(quota.Windows) != 2 { + t.Fatalf("windows = %+v", quota.Windows) + } +} + +func TestLoginCallbackAndRefresh(t *testing.T) { + var refreshed bool + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case oauthTokensPath: + if err := r.ParseForm(); err != nil { + t.Errorf("parse token form: %v", err) + } + if r.Form.Get("grant_type") == "refresh_token" { + refreshed = true + } + if r.Header.Get("DPoP") == "" { + t.Error("token request missing DPoP proof") + } + _, _ = io.WriteString(w, `{"user_id":"user-2","user_name":"login-user","domain_id":"domain-2","refresh_token":"refresh-2","credentials":{"access_key_id":"AK2","secret_access_key":"SK2","security_token":"STS2","expiration":"`+time.Now().Add(24*time.Hour).Format(time.RFC3339Nano)+`"}}`) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + client, store := newTestClient(t, server) + client.stsHost = server.URL + client.portalHost = server.URL + + session, err := client.StartLogin(context.Background(), "acct-1") + if err != nil { + t.Fatalf("StartLogin: %v", err) + } + parsed, err := url.Parse(session.AuthURL) + if err != nil { + t.Fatal(err) + } + port := parsed.Query().Get("port") + if port == "" { + t.Fatalf("auth URL has no port: %s", session.AuthURL) + } + response, err := http.Get("http://127.0.0.1:" + port + OAuthRedirect + "?code=test-code") + if err != nil { + t.Fatalf("callback: %v", err) + } + _ = response.Body.Close() + done, message, err := client.PollLogin(context.Background(), "acct-1") + if err != nil || !done { + t.Fatalf("PollLogin = %v, %q, %v", done, message, err) + } + credential, err := DecodeCredential(store.payload) + if err != nil { + t.Fatal(err) + } + if credential.UserID != "user-2" || credential.SecurityToken != "STS2" || !credential.CanRefresh() { + t.Fatalf("saved credential = %+v", credential) + } + + expired := credential + expired.ExpiresAt = time.Now().Add(-time.Hour).Format(time.RFC3339Nano) + payload, _ := expired.Encode() + store.payload = payload + rotated, err := client.credential(context.Background(), "acct-1") + if err != nil { + t.Fatalf("refresh: %v", err) + } + if !refreshed || rotated.SecurityToken != "STS2" || rotated.RefreshToken != "refresh-2" { + t.Fatalf("rotated = %+v refreshed=%v", rotated, refreshed) + } +} + +func TestProbeAndCheckin(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case currentUserPath: + _, _ = io.WriteString(w, `{"user_id":"probe-user","user_name":"probe","domain_id":"probe-domain"}`) + case creditDeliveryPath: + _, _ = io.WriteString(w, `{"code":0,"data":{"items":[{"campaignId":1,"title":"Daily","status":"ELIGIBLE","claimable":true,"benefitUnit":"CREDIT","benefitAmount":100,"pendingTotalAmount":100,"extra":{"startTime":"2026-01-01T00:00:00Z","endTime":"2027-01-01T00:00:00Z"}}]}}`) + case creditClaimPath, creditConfirmPath: + _, _ = io.WriteString(w, `{"code":0,"data":{}}`) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + client, _ := newTestClient(t, server) + + health, err := client.Probe(context.Background(), "acct-1") + if err != nil { + t.Fatalf("Probe: %v", err) + } + if !health.Ready || health.UID != "probe-user" { + t.Fatalf("health = %+v", health) + } + result, err := client.Checkin(context.Background(), "acct-1") + if err != nil { + t.Fatalf("Checkin: %v", err) + } + if result.Status != "success" || result.RewardCredits != 100 { + t.Fatalf("checkin = %+v", result) + } +} + +func TestImporterAndClassifier(t *testing.T) { + credential, _ := testCredential().Encode() + imported, err := PrepareImport(credential) + if err != nil || !imported.Ready { + t.Fatalf("PrepareImport = %+v, %v", imported, err) + } + if _, err := PrepareImport([]byte(`{"access_key_id":"AK"}`)); err == nil { + t.Fatal("incomplete import accepted") + } + if got := Classify(401, `{"error_msg":"x-auth-token not found"}`); got.Kind != accounts.KindAuth { + t.Fatalf("401 kind = %q", got.Kind) + } + if got := Classify(400, `InferHub.002002009.404 model is not registered`); got.Kind != accounts.KindModelNotAvailable { + t.Fatalf("model kind = %q", got.Kind) + } + if got := Classify(429, `InferHub.4001.429 TPM limit`); got.Kind != accounts.KindRateLimit { + t.Fatalf("rate kind = %q", got.Kind) + } + if got := Classify(400, `InferHub.001001005 request param is invalid`); got.Kind != accounts.KindInvalidRequest { + t.Fatalf("param kind = %q", got.Kind) + } +} + +func TestQuotaPlaceholderTokenMeterIsNotExceeded(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != statisticsPath { + http.NotFound(w, r) + return + } + _, _ = io.WriteString(w, `{"end_date":"2026-11-30","package":{"package_name_en":"Trial"},"metrics":[{"name":"usageTokenChatMessages","show":true,"usage_token_num":193,"package_token_amount":1,"value":0},{"name":"usageTotalPackageCredit","show":true,"package_credit_amount":4500,"package_credit_used":0.27,"package_credit_remain":4499.73,"value":0}]}`) + })) + defer server.Close() + client, _ := newTestClient(t, server) + quota, err := client.Quota(context.Background(), "acct-1") + if err != nil { + t.Fatalf("Quota: %v", err) + } + if quota.Total != 4500 || quota.Remaining != 4499.73 { + t.Fatalf("top-level quota = %+v", quota) + } + for _, window := range quota.Windows { + if window.ID == "usageTokenChatMessages" { + if window.Total != 0 || window.Exceeded { + t.Fatalf("placeholder token meter = %+v", window) + } + } + } +} + +func TestModelsFiltersLearnedUnavailable(t *testing.T) { + credential, _ := testCredential().Encode() + client := NewClient(&memoryStore{payload: credential}) + client.rememberCatalog("acct-1", []providers.ModelInfo{ + {NativeModel: "good", PublicModel: "good"}, + {NativeModel: "bad", PublicModel: "bad"}, + }, nil) + client.markModelUnusable("acct-1", "bad") + models, err := client.Models(context.Background(), "acct-1") + if err != nil { + t.Fatal(err) + } + if len(models) != 1 || models[0].NativeModel != "good" { + t.Fatalf("filtered models = %+v", models) + } +} + +func TestCreditModelsRemainWhenBenefitUnavailable(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case agentListPath: + _, _ = io.WriteString(w, `{"agents":[{"agent_id":"agent-1","agent_name":"CodeAgent","is_primary_agent":true,"show_in_ide":true,"alias":{"alias_zh_cn":"智能体"}}]}`) + case agentDetailPath: + _, _ = io.WriteString(w, `{"gpts":{"models":[{"model_name":"Normal","model_id":"normal-id","model_parameters":{"model_id":"normal-id","context_window":1000,"max_tokens":100}},{"model_name":"Trial","model_id":"trial-id","model_parameters":{"model_id":"trial-id","context_window":1000,"max_tokens":100,"model_desc":"限时体验"}}]}}`) + case modelBuiltinPath: + _, _ = io.WriteString(w, `{"builtinModels":[]}`) + case benefitClaim: + _, _ = io.WriteString(w, `{"error_code":"4000","error_msg":"invalid channel, group not found"}`) + case benefitConfig: + _, _ = io.WriteString(w, `{"error_code":"4000","error_msg":"invalid channel"}`) + case statisticsPath: + _, _ = io.WriteString(w, `{"metrics":[],"model_metrics":[{"model_id":"Trial","model_name":"Trial","model_desc":"限时体验","enable":true}]}`) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + client, _ := newTestClient(t, server) + models, err := client.Models(context.Background(), "acct-1") + if err != nil { + t.Fatal(err) + } + if len(models) != 2 { + t.Fatalf("models = %+v", models) + } + for _, model := range models { + if model.NativeModel == "trial-id" && model.DisplayName != "Trial" { + t.Fatalf("trial display = %+v", model) + } + } +} + +func TestAdapterSurface(t *testing.T) { + credential, _ := testCredential().Encode() + store := &memoryStore{payload: credential} + client := NewClient(store) + adapter := client.Adapter() + for _, capability := range []string{"credential", "login", "chat", "models", "classifier", "import_export", "prober", "checkin"} { + if !adapter.Supports(capability) { + t.Fatalf("adapter missing %s", capability) + } + } + var _ providers.CredentialImporter = importer{} + var _ providers.ModelCatalogProvider = client + var _ providers.ProviderChat = client + var _ providers.AccountProber = client +} diff --git a/internal/providers/codearts/constants.go b/internal/providers/codearts/constants.go new file mode 100644 index 00000000..d32c6ae6 --- /dev/null +++ b/internal/providers/codearts/constants.go @@ -0,0 +1,84 @@ +package codearts + +import "time" + +// ProviderID is the provider family id registered in the control plane. +const ProviderID = "codearts" + +// CredentialFormat is the canonical stored credential format. +const CredentialFormat = "codearts-oauth-v1" + +// ClientID is the OAuth client used by the official CodeArts Agent extension. +// The refresh token is bound to this value; refreshing with a different id is +// rejected by STS. +const ClientID = "codearts-agent" + +// Endpoints. The chat gateway and the token service are different hosts: +// chat is the snap-access regional gateway, while OAuth token exchange and +// refresh go through the regional STS endpoint. +const ( + CNBaseURL = "https://snap-access.cn-north-4.myhuaweicloud.com" + CNSTSHost = "https://sts.cn-north-4.myhuaweicloud.com" + CNPortalHost = "https://codearts.huaweicloud.com/portal" + BenefitHost = "https://opengw.developer.huaweicloud.com" +) + +// API paths. Paths are relative to CNBaseURL unless noted. +const ( + chatPath = "/api/v2/chat/completions" + loginTicketPath = "/snap-manager/v1/login/ticket" + currentUserPath = "/snap-manager/v1/current/user" + statisticsPath = "/snap-manager/v1/statistics/plugin" + modelBuiltinPath = "/v1/model/builtin" + thinkingVariants = "/v1/model/thinking-variants" + agentListPath = "/v1/agent-center/agents/useragents" + agentDetailPath = "/v1/agent-center/agents/detail" + creditDeliveryPath = "/v1/ops/delivery" + creditClaimPath = "/v1/ops/claim" + creditConfirmPath = "/v1/ops/confirm" +) + +// OAuth and benefit paths. +const ( + oauthTokensPath = "/v1/oauth2/tokens" + benefitConfig = "/api/v1/gateway/config" + benefitClaim = "/api/v1/benefit/claim" + benefitBalance = "/api/v1/user/tokens/balance" +) + +// Client identity reported to the OAuth portal and gateway. +const ( + PluginName = "snap_AIIDE" + PluginVersion = "5.2.0" + OAuthRedirect = "/oauth/callback" + Language = "zh-cn" + AppID = "CodeAgent3.0" +) + +// Timing knobs. +const ( + // refreshLead refreshes the STS credential this long before expiry so a + // long stream never starts on a credential that dies mid-response. + refreshLead = 15 * time.Minute + // metadataTimeout bounds model / statistics / identity calls. + metadataTimeout = 30 * time.Second + // chatTimeout bounds one streaming HTTP request when the caller does not + // set a shorter context. + chatTimeout = 10 * time.Minute + // firstByteTimeout is the maximum wait for response headers plus the first + // SSE event. It is returned as providers.ErrFirstByteTimeout, never as a + // context deadline, so the executor can fail over. + firstByteTimeout = 90 * time.Second + // catalogTTL caps how long a discovered model list is reused. + catalogTTL = 30 * time.Minute + // loginTTL bounds one browser login round. + loginTTL = 15 * time.Minute + // maxBodyBytes caps upstream error bodies retained for classification. + maxBodyBytes = 1 << 20 +) + +// HeaderMaasType routes a request to the limited-benefit model gateway. +const ( + HeaderMaasType = "maas_type" + MaasBenefit = "benefit" +) diff --git a/internal/providers/codearts/credential.go b/internal/providers/codearts/credential.go new file mode 100644 index 00000000..c2ec6f15 --- /dev/null +++ b/internal/providers/codearts/credential.go @@ -0,0 +1,300 @@ +package codearts + +import ( + "encoding/json" + "errors" + "fmt" + "net/url" + "strings" + "time" + + "github.com/caigee-cmd/cli2api/internal/providers" +) + +// Credential is the canonical storage payload. The field names match the +// upstream OAuth response and the credential files written by the official +// CodeArts integrations, so an exported auth file can be pasted directly. +type Credential struct { + Format string `json:"format"` + AccessKeyID string `json:"access_key_id"` + SecretAccessKey string `json:"secret_access_key"` + SecurityToken string `json:"security_token,omitempty"` + ExpiresAt string `json:"expires_at,omitempty"` + DomainID string `json:"domain_id,omitempty"` + UserID string `json:"user_id,omitempty"` + UserName string `json:"user_name,omitempty"` + RefreshToken string `json:"refresh_token,omitempty"` + CodeVerifier string `json:"code_verifier,omitempty"` + ClientID string `json:"client_id,omitempty"` + DPoPPrivateKey map[string]string `json:"dpop_private_key,omitempty"` + Plan string `json:"plan,omitempty"` + BaseURL string `json:"base_url,omitempty"` +} + +// wireCredential accepts the aliases seen in exported credential files: the +// STS trio may be nested under credentials/credential, security_token may be +// cloud_dragon_token, and expiry may be expiration/expires_at. +type wireCredential struct { + Format string `json:"format"` + AccessKeyID string `json:"access_key_id"` + SecretAccessKey string `json:"secret_access_key"` + SecurityToken string `json:"security_token"` + CloudDragonTok string `json:"cloud_dragon_token"` + ExpiresAt string `json:"expires_at"` + Expiration string `json:"expiration"` + DomainID string `json:"domain_id"` + AccountID string `json:"account_id"` + UserID string `json:"user_id"` + UserName string `json:"user_name"` + RefreshToken string `json:"refresh_token"` + CodeVerifier string `json:"code_verifier"` + ClientID string `json:"client_id"` + DPoPPrivateKey map[string]string `json:"dpop_private_key"` + Plan string `json:"plan"` + BaseURL string `json:"base_url"` + Credentials struct { + AccessKeyID string `json:"access_key_id"` + SecretAccessKey string `json:"secret_access_key"` + SecurityToken string `json:"security_token"` + Expiration string `json:"expiration"` + } `json:"credentials"` + Credential struct { + Access string `json:"access"` + Secret string `json:"secret"` + SecurityToken string `json:"securitytoken"` + ExpiresAt string `json:"expires_at"` + } `json:"credential"` + OAuthContext struct { + PKCEPair struct { + CodeVerifier string `json:"code_verifier"` + } `json:"pkce_pair"` + DPoPKeyPair struct { + PrivateKeyJWK map[string]string `json:"private_key_jwk"` + } `json:"dpop_key_pair"` + } `json:"oauth_context"` +} + +// DecodeCredential accepts the canonical shape plus the aliases above. +func DecodeCredential(payload []byte) (Credential, error) { + var wrapper map[string]json.RawMessage + if json.Unmarshal(payload, &wrapper) == nil { + if nested, ok := wrapper["codearts_provider_credential"]; ok { + return DecodeCredential(nested) + } + } + var wire wireCredential + if err := json.Unmarshal(payload, &wire); err != nil { + return Credential{}, fmt.Errorf("codearts credential is not valid json") + } + credential := Credential{ + Format: firstNonEmpty(wire.Format, CredentialFormat), + AccessKeyID: strings.TrimSpace(wire.AccessKeyID), + SecretAccessKey: strings.TrimSpace(wire.SecretAccessKey), + SecurityToken: strings.TrimSpace(firstNonEmpty(wire.SecurityToken, wire.CloudDragonTok)), + ExpiresAt: strings.TrimSpace(firstNonEmpty(wire.ExpiresAt, wire.Expiration)), + DomainID: strings.TrimSpace(firstNonEmpty(wire.DomainID, wire.AccountID)), + UserID: strings.TrimSpace(wire.UserID), + UserName: strings.TrimSpace(wire.UserName), + RefreshToken: strings.TrimSpace(wire.RefreshToken), + CodeVerifier: strings.TrimSpace(wire.CodeVerifier), + ClientID: strings.TrimSpace(wire.ClientID), + DPoPPrivateKey: cloneStringMap(wire.DPoPPrivateKey), + Plan: strings.TrimSpace(wire.Plan), + BaseURL: normalizeBase(wire.BaseURL), + } + if credential.AccessKeyID == "" { + credential.AccessKeyID = strings.TrimSpace(wire.Credentials.AccessKeyID) + } + if credential.SecretAccessKey == "" { + credential.SecretAccessKey = strings.TrimSpace(wire.Credentials.SecretAccessKey) + } + if credential.SecurityToken == "" { + credential.SecurityToken = strings.TrimSpace(wire.Credentials.SecurityToken) + } + if credential.ExpiresAt == "" { + credential.ExpiresAt = strings.TrimSpace(wire.Credentials.Expiration) + } + if credential.AccessKeyID == "" { + credential.AccessKeyID = strings.TrimSpace(wire.Credential.Access) + } + if credential.SecretAccessKey == "" { + credential.SecretAccessKey = strings.TrimSpace(wire.Credential.Secret) + } + if credential.SecurityToken == "" { + credential.SecurityToken = strings.TrimSpace(wire.Credential.SecurityToken) + } + if credential.ExpiresAt == "" { + credential.ExpiresAt = strings.TrimSpace(wire.Credential.ExpiresAt) + } + if credential.CodeVerifier == "" { + credential.CodeVerifier = strings.TrimSpace(wire.OAuthContext.PKCEPair.CodeVerifier) + } + if len(credential.DPoPPrivateKey) == 0 { + credential.DPoPPrivateKey = cloneStringMap(wire.OAuthContext.DPoPKeyPair.PrivateKeyJWK) + } + return credential, nil +} + +// Encode writes the canonical payload. +func (c Credential) Encode() ([]byte, error) { + c.Format = CredentialFormat + c.AccessKeyID = strings.TrimSpace(c.AccessKeyID) + c.SecretAccessKey = strings.TrimSpace(c.SecretAccessKey) + c.SecurityToken = strings.TrimSpace(c.SecurityToken) + c.ExpiresAt = strings.TrimSpace(c.ExpiresAt) + c.RefreshToken = strings.TrimSpace(c.RefreshToken) + c.CodeVerifier = strings.TrimSpace(c.CodeVerifier) + c.ClientID = strings.TrimSpace(c.ClientID) + c.BaseURL = normalizeBase(c.BaseURL) + return json.Marshal(c) +} + +// Ready reports whether the credential can authenticate a chat request now. +// A credential with only a refresh token is not ready until refresh succeeds. +func (c Credential) Ready() bool { + return strings.TrimSpace(c.AccessKeyID) != "" && + strings.TrimSpace(c.SecretAccessKey) != "" && + strings.TrimSpace(c.SecurityToken) != "" +} + +// Valid reports whether the signing material exists. Unlike Ready it does not +// require the STS token, so an expired credential can still be refreshed. +func (c Credential) Valid() bool { + return strings.TrimSpace(c.AccessKeyID) != "" && strings.TrimSpace(c.SecretAccessKey) != "" +} + +// CanRefresh reports whether the long-lived half needed for refresh is present. +func (c Credential) CanRefresh() bool { + return strings.TrimSpace(c.RefreshToken) != "" && strings.TrimSpace(c.CodeVerifier) != "" +} + +// NeedsRefresh reports whether the STS credential should be rotated before use. +// An empty or unparseable expiry is treated as stale (not fresh), because a bad +// timestamp would otherwise hide an expired credential forever. +func (c Credential) NeedsRefresh(now time.Time) bool { + if !c.Ready() { + return c.CanRefresh() + } + raw := strings.TrimSpace(c.ExpiresAt) + if raw == "" { + return false + } + expiry, err := time.Parse(time.RFC3339Nano, raw) + if err != nil { + return true + } + return !now.Add(refreshLead).Before(expiry) +} + +// Expiry returns the parsed expiry, or the zero time. +func (c Credential) Expiry() time.Time { + expiry, err := time.Parse(time.RFC3339Nano, strings.TrimSpace(c.ExpiresAt)) + if err != nil { + return time.Time{} + } + return expiry +} + +// Sign returns the AK/SK material used for request signing. +func (c Credential) Sign() SignCredential { + return SignCredential{ + AccessKeyID: c.AccessKeyID, + SecretAccessKey: c.SecretAccessKey, + SecurityToken: c.SecurityToken, + } +} + +// AuthBase returns the optional per-credential gateway override (tests only). +func (c Credential) AuthBase() string { return normalizeBase(c.BaseURL) } + +// ValidateCredential is the CredentialCodec entry point. +func ValidateCredential(payload []byte) error { + credential, err := DecodeCredential(payload) + if err != nil { + return err + } + if !credential.Ready() && !credential.CanRefresh() { + return errors.New("codearts credential requires STS access keys or a refresh token") + } + if credential.BaseURL != "" { + if err := validateBaseURL(credential.BaseURL); err != nil { + return err + } + } + return nil +} + +// validateBaseURL rejects non-https or malformed overrides. Production never +// sets a base URL; it exists so package tests can drive an httptest origin. +func validateBaseURL(raw string) error { + parsed, err := url.Parse(raw) + if err != nil || parsed.Host == "" { + return fmt.Errorf("codearts endpoint must be an absolute URL") + } + if parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" { + return fmt.Errorf("codearts endpoint must not carry credentials, a query, or a fragment") + } + if !strings.EqualFold(parsed.Scheme, "https") && !acceptTestEndpoint { + return fmt.Errorf("codearts endpoint must be an https URL") + } + if !acceptTestEndpoint { + host := strings.ToLower(parsed.Hostname()) + if host != "myhuaweicloud.com" && !strings.HasSuffix(host, ".myhuaweicloud.com") && + host != "huaweicloud.com" && !strings.HasSuffix(host, ".huaweicloud.com") { + return fmt.Errorf("codearts endpoint must be a Huawei Cloud host") + } + } + return nil +} + +// acceptTestEndpoint lets package tests drive the adapter against httptest. +var acceptTestEndpoint = false + +func normalizeBase(raw string) string { + return strings.TrimRight(strings.TrimSpace(raw), "/") +} + +func cloneStringMap(in map[string]string) map[string]string { + if len(in) == 0 { + return nil + } + out := make(map[string]string, len(in)) + for k, v := range in { + out[k] = v + } + return out +} + +func firstNonEmpty(values ...string) string { + for _, value := range values { + if strings.TrimSpace(value) != "" { + return strings.TrimSpace(value) + } + } + return "" +} + +// importFormat is the credential format accepted by the importer. +func importFormat() string { return CredentialFormat } + +// PrepareImport validates and normalizes a pasted credential. The Ready flag is +// true when the credential can either chat now or refresh itself. +func PrepareImport(payload []byte) (providers.CredentialImport, error) { + credential, err := DecodeCredential(payload) + if err != nil { + return providers.CredentialImport{}, err + } + if !credential.Ready() && !credential.CanRefresh() { + return providers.CredentialImport{}, errors.New("codearts credential requires STS access keys or a refresh token") + } + if credential.BaseURL != "" { + if err := validateBaseURL(credential.BaseURL); err != nil { + return providers.CredentialImport{}, err + } + } + encoded, err := credential.Encode() + if err != nil { + return providers.CredentialImport{}, err + } + return providers.CredentialImport{Payload: encoded, Ready: true}, nil +} diff --git a/internal/providers/codearts/doc.go b/internal/providers/codearts/doc.go new file mode 100644 index 00000000..c658c1e8 --- /dev/null +++ b/internal/providers/codearts/doc.go @@ -0,0 +1,98 @@ +// Package codearts implements the Huawei Cloud CodeArts Agent (码道) in-process +// provider. +// +// CodeArts has four client forms (IDE, IDE extension, JetBrains plugin, CLI). +// This adapter implements the cloud-facing protocol they all share, not the +// CLI's local agent server. The protocol facts below were verified against +// codearts2api (MIT, commit 8d1e43d) and re-checked against the locally +// installed CodeArts Agent kernel; see NOTICE. +// +// # Login +// +// Huawei's public docs describe AK/SK for the CLI, but the personal-account +// OAuth flow is narrower and is what the official extension and the CLI use for +// chat: the portal at codearts.huaweicloud.com/portal issues an authorization +// code (PKCE S256, client_id codearts-agent, local callback +// 127.0.0.1:/oauth/callback), and the code is exchanged at +// sts.cn-north-4.myhuaweicloud.com/v1/oauth2/tokens for a short-lived STS trio +// (access_key_id, secret_access_key, security_token). The refresh token is +// bound to the PKCE verifier and the DPoP P-256 key used for the first +// exchange, so both are stored with the credential. A rejected refresh +// surfaces as an auth failure and asks for a new login; a successful rotation +// is persisted before the request continues. +// +// Import is supported; export is not. A credential contains the STS keys and +// the refresh token, so handing it back out of the console would hand out the +// account. The descriptor advertises ImportExport because that capability +// covers the import side; the console export action reports unsupported. +// +// # Chat +// +// Chat is a direct HTTP/SSE OpenAI-compatible call to +// snap-access.cn-north-4.myhuaweicloud.com/api/v2/chat/completions. The request +// carries x-auth-token (the STS security token) and is signed with Huawei's +// SDK-HMAC-SHA256 AK/SK scheme. The body uses the native OpenAI shape; tools, +// tool_choice, reasoning_effort, max_tokens, temperature and top_p all pass +// through. tool_choice accepts only the string forms, so a named function +// choice is sent as "required" with the tools list narrowed to that function. +// Assistant tool calls and tool results are forwarded only when they form a +// complete pair; CodeArts rejects orphaned tool history with a 400 and then +// keeps failing every later turn. +// +// Streams are CodeArts/OpenAI SSE events. The adapter normalizes the event +// names and emits standard chat-completions deltas, a final usage chunk when +// the upstream supplies usage, and a single [DONE]. The non-stream path +// aggregates the same stream, so usage, reasoning and tool-call assembly share +// one parser. +// +// # Catalog and quota +// +// The catalog is per account and merges three upstream sources: the CodeAgent +// detail's gpts.models, GET /v1/model/builtin (Agent-Type: PromptCenter), and +// the limited-benefit gateway config. Each record declares its exact model id +// plus context/output limits; those declarations are the only capability +// source. The adapter declares tools because the chat endpoint natively +// accepts them, and forwards image_url content parts for models whose catalog +// entry advertises supports_images. A text-only model rejects image input with +// a 400 instead of silently dropping it. GET /v1/model/thinking-variants +// supplies each model's allowed reasoning strengths; the adapter exposes them +// as ReasoningOptions and forwards the selected level as reasoning_effort. +// Benefits models are marked and routed with maas_type: benefit; the adapter +// claims the benefit idempotently while discovering the catalog, mirroring the +// official client opening its model menu. +// +// The discovered list is not the same as the display list: CodeArts' agent +// and builtin catalogs are keyed by model_parameters.model_id, while +// model_name is only the label. Sending the display name (for example +// GLM-5.2-ArkTS-SPARK instead of glm-5.2-sft-harmony) returns 002002009. The +// adapter keeps model_id as NativeModel and model_name only as DisplayName. +// +// Benefits are a separate routing path. A model advertised by a successful +// benefit gateway/config is marked and sent with maas_type: benefit. A +// credit-only trial account has no benefit group: claim/config answer 4000 +// invalid channel/group not found, and adding maas_type: benefit to the same +// model returns 4004.200 benefit not found. Crucially, the same model works +// over the credit path without that header. Therefore benefit discovery is +// additive and a failed claim/config never hides agent/builtin models. Only a +// real 002002009/4004.200 chat answer marks that model unusable for 30 minutes +// and removes it from /v1/models; the account stays ready because this is an +// entitlement failure, not auth. +// +// The catalog endpoint's values are capped for output: the upstream rejects +// max_tokens above 65536 even when the catalog advertises more, so the public +// ceiling is min(declared, 65536). +// +// GET /snap-manager/v1/statistics/plugin supplies the package meters +// (usageTotalPackageCredit, usageTokenChatMessages, ...). The adapter maps the +// visible meters into quota windows; the package has no per-meter expiry, so +// the FEFO layer treats it as unknown rather than inventing an end date. +// codearts stats is only a local session ledger and is not used. +// +// # Check-in +// +// The daily check-in and new-user credits are two credit campaigns delivered by +// the ops endpoints (/v1/ops/delivery, /v1/ops/claim, /v1/ops/confirm), not by +// the CLI. Claiming is idempotent and the confirm step is retried separately; +// the adapter reports the claimed amount and never presents it as the account +// balance. +package codearts diff --git a/internal/providers/codearts/dpop.go b/internal/providers/codearts/dpop.go new file mode 100644 index 00000000..8040dee0 --- /dev/null +++ b/internal/providers/codearts/dpop.go @@ -0,0 +1,123 @@ +package codearts + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "math/big" + "time" +) + +// dpopKeyPair is the ES256 key pair used in the DPoP proof sent to STS. +type dpopKeyPair struct { + PrivateKey *ecdsa.PrivateKey + PublicJWK map[string]string +} + +// newDPoPPrivateJWK generates a persistable P-256 private key. STS binds the +// refresh token to the public half, so the same JWK must be reused on refresh. +func newDPoPPrivateJWK() (map[string]string, error) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + return map[string]string{ + "kty": "EC", + "crv": "P-256", + "x": base64.RawURLEncoding.EncodeToString(padded(key.PublicKey.X, 32)), + "y": base64.RawURLEncoding.EncodeToString(padded(key.PublicKey.Y, 32)), + "d": base64.RawURLEncoding.EncodeToString(padded(key.D, 32)), + }, nil +} + +func dpopKeyPairFor(jwk map[string]string) (*dpopKeyPair, error) { + if len(jwk) == 0 || jwk["d"] == "" { + generated, err := newDPoPPrivateJWK() + if err != nil { + return nil, err + } + jwk = generated + } + if jwk["kty"] != "EC" || jwk["crv"] != "P-256" { + return nil, fmt.Errorf("invalid DPoP JWK curve") + } + decode := func(field string) ([]byte, error) { + raw, err := base64.RawURLEncoding.DecodeString(jwk[field]) + if err != nil || len(raw) != 32 { + return nil, fmt.Errorf("invalid DPoP JWK %s", field) + } + return raw, nil + } + xRaw, err := decode("x") + if err != nil { + return nil, err + } + yRaw, err := decode("y") + if err != nil { + return nil, err + } + dRaw, err := decode("d") + if err != nil { + return nil, err + } + curve := elliptic.P256() + x, y, d := new(big.Int).SetBytes(xRaw), new(big.Int).SetBytes(yRaw), new(big.Int).SetBytes(dRaw) + if d.Sign() <= 0 || d.Cmp(curve.Params().N) >= 0 || !curve.IsOnCurve(x, y) { + return nil, fmt.Errorf("invalid DPoP JWK key material") + } + wantX, wantY := curve.ScalarBaseMult(dRaw) + if wantX.Cmp(x) != 0 || wantY.Cmp(y) != 0 { + return nil, fmt.Errorf("DPoP JWK public/private key mismatch") + } + return &dpopKeyPair{ + PrivateKey: &ecdsa.PrivateKey{PublicKey: ecdsa.PublicKey{Curve: curve, X: x, Y: y}, D: d}, + PublicJWK: map[string]string{"kty": "EC", "crv": "P-256", "x": jwk["x"], "y": jwk["y"]}, + }, nil +} + +// signDPoPProof builds the RFC 9449 proof for the token endpoint. +func signDPoPProof(kp *dpopKeyPair, htu string) (string, error) { + header, _ := json.Marshal(map[string]any{ + "alg": "ES256", + "typ": "dpop+jwt", + "jwk": kp.PublicJWK, + }) + payload, _ := json.Marshal(map[string]any{ + "htm": "POST", + "htu": htu, + "iat": time.Now().Unix(), + "jti": randomHex(32), + }) + input := base64.RawURLEncoding.EncodeToString(header) + "." + base64.RawURLEncoding.EncodeToString(payload) + digest := sha256.Sum256([]byte(input)) + r, s, err := ecdsa.Sign(rand.Reader, kp.PrivateKey, digest[:]) + if err != nil { + return "", err + } + n := elliptic.P256().Params().N + halfN := new(big.Int).Rsh(n, 1) + if s.Cmp(halfN) > 0 { + s.Sub(n, s) + } + sig := append(padded(r, 32), padded(s, 32)...) + return input + "." + base64.RawURLEncoding.EncodeToString(sig), nil +} + +func padded(b *big.Int, size int) []byte { + out := make([]byte, size) + raw := b.Bytes() + copy(out[size-len(raw):], raw) + return out +} + +func randomHex(n int) string { + b := make([]byte, n) + if _, err := rand.Read(b); err != nil { + return fmt.Sprintf("%x", time.Now().UnixNano()) + } + return fmt.Sprintf("%x", b) +} diff --git a/internal/providers/codearts/errors.go b/internal/providers/codearts/errors.go new file mode 100644 index 00000000..bdc2f28a --- /dev/null +++ b/internal/providers/codearts/errors.go @@ -0,0 +1,128 @@ +package codearts + +import ( + "net/http" + "regexp" + "strings" + + "github.com/caigee-cmd/cli2api/internal/accounts" + "github.com/caigee-cmd/cli2api/internal/providers" +) + +var secretPatterns = []*regexp.Regexp{ + regexp.MustCompile(`(?i)(x-auth-token|x-security-token|authorization)\s*[:=]\s*\S+`), + regexp.MustCompile(`(?i)Access=[^,\s]+`), + regexp.MustCompile(`(?i)Signature=[0-9a-f]+`), + regexp.MustCompile(`(?i)bearer\s+\S+`), +} + +func redactSecrets(text string) string { + safe := text + for _, pattern := range secretPatterns { + safe = pattern.ReplaceAllString(safe, "[redacted]") + } + return safe +} + +var modelUnavailableMarkers = []string{ + "inferhub.002002009", + "not registered", + "4004.200", + "benefit not found", + "model is not available", + "model not found", + "does not have access", +} + +var invalidRequestMarkers = []string{ + "inferhub.001001005", + "request param is invalid", + "invalid request", + "tool_choice", +} + +var quotaMarkers = []string{ + "insufficient quota", + "quota exceeded", + "quota exhausted", + "insufficient balance", + "余额不足", + "积分不足", +} + +var rateLimitMarkers = []string{ + "tm.00001041", + "too many requests", + "rate limit", + "concurrent session", + "并发会话", + "tpm", + "queue", +} + +// Classify maps CodeArts error bodies onto the executor taxonomy. A model the +// account cannot use is model_not_available and keeps the account ready; an +// expired STS token is auth and asks for a refresh/login. +func Classify(status int, body string) providers.ClassifiedError { + safe := redactSecrets(strings.TrimSpace(body)) + lower := strings.ToLower(safe) + message := firstNonEmpty(safe, "codearts upstream error") + switch { + case containsAny(lower, modelUnavailableMarkers): + return providers.ClassifiedError{Kind: accounts.KindModelNotAvailable, Status: http.StatusNotFound, Message: message} + case containsAny(lower, quotaMarkers) || status == http.StatusPaymentRequired: + return providers.ClassifiedError{Kind: accounts.KindQuota, Status: http.StatusPaymentRequired, Message: message} + case status == http.StatusUnauthorized || status == http.StatusForbidden || + strings.Contains(lower, "apig.0301") || strings.Contains(lower, "x-auth-token") || + strings.Contains(lower, "token expired") || strings.Contains(lower, "invalid token"): + return providers.ClassifiedError{Kind: accounts.KindAuth, Status: http.StatusUnauthorized, Message: message} + case containsAny(lower, rateLimitMarkers) || status == http.StatusTooManyRequests: + return providers.ClassifiedError{Kind: accounts.KindRateLimit, Status: http.StatusTooManyRequests, Message: message} + case containsAny(lower, invalidRequestMarkers) || status == http.StatusBadRequest || status == http.StatusUnprocessableEntity: + return providers.ClassifiedError{Kind: accounts.KindInvalidRequest, Status: http.StatusBadRequest, Message: message} + case status == http.StatusNotFound: + return providers.ClassifiedError{Kind: accounts.KindModelNotAvailable, Status: http.StatusNotFound, Message: message} + case status == 499 || strings.Contains(lower, "canceled") || strings.Contains(lower, "cancelled"): + return providers.ClassifiedError{Kind: accounts.KindCanceled, Status: 499, Message: message} + case status >= 500 || status == 0: + return providers.ClassifiedError{Kind: accounts.KindUnavailable, Status: http.StatusBadGateway, Message: message} + } + if status >= 400 { + return providers.ClassifiedError{Kind: accounts.KindUnavailable, Status: status, Message: message} + } + return providers.ClassifiedError{} +} + +func containsAny(text string, markers []string) bool { + for _, marker := range markers { + if strings.Contains(text, marker) { + return true + } + } + return false +} + +// newProviderError turns an HTTP status/body into the provider error the +// executor classifies. The message is always redacted. +func newProviderError(status int, body string) *providers.Error { + classified := Classify(status, body) + kind := classified.Kind + if kind == "" { + kind = accounts.KindUnavailable + } + code := http.StatusBadGateway + if classified.Status != 0 { + code = classified.Status + } + message := classified.Message + if message == "" { + message = "codearts upstream error" + } + return &providers.Error{Kind: kind, Status: code, Message: message} +} + +type classifier struct{} + +func (classifier) Classify(status int, body string) providers.ClassifiedError { + return Classify(status, body) +} diff --git a/internal/providers/codearts/import.go b/internal/providers/codearts/import.go new file mode 100644 index 00000000..b492f371 --- /dev/null +++ b/internal/providers/codearts/import.go @@ -0,0 +1,23 @@ +package codearts + +import ( + "context" + + "github.com/caigee-cmd/cli2api/internal/providers" +) + +type importer struct{} + +func (importer) Format() string { return CredentialFormat } + +func (importer) ValidateImport(payload []byte) error { return ValidateCredential(payload) } + +func (importer) PrepareImport(payload []byte) (providers.CredentialImport, error) { + return PrepareImport(payload) +} + +// Export is intentionally unsupported. A CodeArts credential contains the STS +// keys and the refresh token; exporting it would hand out the account. +func (importer) Export(context.Context, string) (map[string]any, error) { + return nil, providers.ErrUnsupported +} diff --git a/internal/providers/codearts/oauth.go b/internal/providers/codearts/oauth.go new file mode 100644 index 00000000..adf04b95 --- /dev/null +++ b/internal/providers/codearts/oauth.go @@ -0,0 +1,374 @@ +package codearts + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strings" + "sync" + "time" + + "github.com/caigee-cmd/cli2api/internal/accounts" + "github.com/caigee-cmd/cli2api/internal/auth" + "github.com/caigee-cmd/cli2api/internal/providers" +) + +// loginState is one in-flight browser authorization. The local callback +// listener receives the authorization code; the ticket polling fallback is +// still attempted when the portal answers with a secret instead. +type loginState struct { + authURL string + ticketID string + secret string + verifier string + dpop map[string]string + port int + expiresAt time.Time + listener net.Listener + + mu sync.Mutex + done bool + err error + completed chan struct{} +} + +func (s *loginState) finish(err error) { + s.mu.Lock() + if s.done { + s.mu.Unlock() + return + } + s.done = true + s.err = err + s.mu.Unlock() + close(s.completed) +} + +func (s *loginState) result() (bool, error) { + s.mu.Lock() + defer s.mu.Unlock() + return s.done, s.err +} + +// StartLogin builds the portal URL and starts the loopback listener. The +// console shows the URL; PollLogin waits for the callback to finish. +func (c *Client) StartLogin(ctx context.Context, accountID string) (providers.LoginSession, error) { + c.cleanupLogins() + ticketID := randomHex(16) + verifier, challenge, err := pkcePair() + if err != nil { + return providers.LoginSession{}, err + } + dpop, err := newDPoPPrivateJWK() + if err != nil { + return providers.LoginSession{}, err + } + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return providers.LoginSession{}, fmt.Errorf("codearts login listener: %w", err) + } + port := listener.Addr().(*net.TCPAddr).Port + authURL := c.buildAuthorizeURL(ticketID, challenge, port) + state := &loginState{ + authURL: authURL, + ticketID: ticketID, + verifier: verifier, + dpop: dpop, + port: port, + expiresAt: c.now().Add(loginTTL), + listener: listener, + completed: make(chan struct{}), + } + auth.ServeLoopback(listener, OAuthRedirect, "CodeArts", func(callbackCtx context.Context, rawURL string) error { + return c.handleCallback(callbackCtx, accountID, state, rawURL) + }) + c.mu.Lock() + if c.logins == nil { + c.logins = map[string]*loginState{} + } + if previous := c.logins[accountID]; previous != nil { + previous.finish(fmt.Errorf("superseded by a new login round")) + _ = previous.listener.Close() + } + c.logins[accountID] = state + c.mu.Unlock() + return providers.LoginSession{AuthURL: authURL, State: ticketID}, nil +} + +// PollLogin advances one login round. It returns done=true only after the +// credential has been persisted. +func (c *Client) PollLogin(ctx context.Context, accountID string) (bool, string, error) { + c.mu.Lock() + state := c.logins[accountID] + c.mu.Unlock() + if state == nil { + return false, "", fmt.Errorf("login not started for account %s", accountID) + } + select { + case <-state.completed: + done, err := state.result() + if err != nil { + c.clearLogin(accountID) + return false, "", err + } + if done { + c.clearLogin(accountID) + return true, "login complete", nil + } + case <-time.After(time.Second): + } + if c.now().After(state.expiresAt) { + c.clearLogin(accountID) + return false, "", fmt.Errorf("codearts login timed out; start again") + } + state.mu.Lock() + secret := state.secret + state.mu.Unlock() + if secret != "" { + if done, message, err := c.pollTicket(ctx, accountID, state, secret); err != nil { + return false, "", err + } else if done { + return true, message, nil + } + } + return false, "waiting for browser authorization", nil +} + +// CompleteLogin accepts a callback URL pasted by the operator when the +// browser cannot reach the proxy host. +func (c *Client) CompleteLogin(ctx context.Context, accountID, callbackURL string) error { + c.mu.Lock() + state := c.logins[accountID] + c.mu.Unlock() + if state == nil { + return fmt.Errorf("login not started for account %s", accountID) + } + parsed, err := url.Parse(strings.TrimSpace(callbackURL)) + if err != nil || parsed.Scheme != "http" || parsed.User != nil || parsed.Path != OAuthRedirect { + return fmt.Errorf("callback URL must be the CodeArts http://127.0.0.1 callback") + } + host := parsed.Hostname() + if host != "127.0.0.1" && host != "localhost" && host != "::1" { + return fmt.Errorf("callback URL must point at a loopback host") + } + if parsed.Port() != fmt.Sprint(state.port) { + return fmt.Errorf("callback port does not match this login round") + } + code := parsed.Query().Get("code") + if code == "" { + return fmt.Errorf("callback URL has no authorization code") + } + if err := c.completeCode(ctx, accountID, state, code); err != nil { + return err + } + return nil +} + +func (c *Client) handleCallback(ctx context.Context, accountID string, state *loginState, rawURL string) error { + parsed, err := url.Parse(rawURL) + if err != nil { + return err + } + query := parsed.Query() + if code := query.Get("code"); code != "" { + return c.completeCode(ctx, accountID, state, code) + } + if secret := query.Get("secret"); secret != "" { + state.mu.Lock() + state.secret = secret + state.mu.Unlock() + return nil + } + return fmt.Errorf("callback is missing code or secret") +} + +func (c *Client) completeCode(ctx context.Context, accountID string, state *loginState, code string) error { + credential := Credential{ + Format: CredentialFormat, + ClientID: ClientID, + CodeVerifier: state.verifier, + DPoPPrivateKey: state.dpop, + } + token, err := c.exchangeCode(ctx, credential, code, state.port) + if err != nil { + state.finish(err) + return err + } + credential = applyToken(credential, token) + if credential.UserID == "" || credential.UserName == "" { + if uid, name, domain, userErr := c.currentUser(ctx, credential); userErr == nil { + credential.UserID = firstNonEmpty(credential.UserID, uid) + credential.UserName = firstNonEmpty(credential.UserName, name) + credential.DomainID = firstNonEmpty(credential.DomainID, domain) + } + } + if err := c.saveCredential(ctx, accountID, credential); err != nil { + state.finish(err) + return err + } + _ = c.store.Observe(ctx, accountID, credential.UserID, "ready", "", "") + state.finish(nil) + return nil +} + +func (c *Client) exchangeCode(ctx context.Context, credential Credential, code string, port int) (*tokenResponse, error) { + redirect := fmt.Sprintf("http://127.0.0.1:%d%s", port, OAuthRedirect) + token, err := c.requestToken(ctx, map[string][]string{ + "client_id": {ClientID}, + "code": {code}, + "code_verifier": {credential.CodeVerifier}, + "grant_type": {"authorization_code"}, + "redirect_uri": {redirect}, + }, credential) + if err != nil { + if providerErr, ok := err.(*providers.Error); ok && providerErr.Kind == accounts.KindAuth { + return nil, err + } + return nil, err + } + return token, nil +} + +func (c *Client) buildAuthorizeURL(ticketID, challenge string, port int) string { + values := url.Values{} + values.Set("theme", "2") + values.Set("locale", Language) + values.Set("uri_scheme", ClientID) + values.Set("client_id", ClientID) + values.Set("port", fmt.Sprint(port)) + values.Set("code_challenge", challenge) + values.Set("code_challenge_method", "SHA-256") + values.Set("ticket_id", ticketID) + values.Set("plugin-name", PluginName) + values.Set("plugin-version", PluginVersion) + return c.portalHost + "/authorize?" + values.Encode() +} + +// pollTicket is the legacy fallback: the portal calls back with a secret, and +// the ticket endpoint returns credentials. It is only used when no code ever +// arrives. +func (c *Client) pollTicket(ctx context.Context, accountID string, state *loginState, secret string) (bool, string, error) { + values := url.Values{} + values.Set("ticket_id", state.ticketID) + values.Set("secret", secret) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.apiBase(Credential{})+loginTicketPath+"?"+values.Encode(), nil) + if err != nil { + return false, "", err + } + req.Header.Set("plugin-name", PluginName) + req.Header.Set("plugin-version", PluginVersion) + client, err := c.httpClient(ctx, accountID) + if err != nil { + return false, "", err + } + resp, err := client.Do(req) + if err != nil { + return false, "", err + } + defer resp.Body.Close() + raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if resp.StatusCode >= 400 { + return false, "", nil + } + var token tokenResponse + if err := json.Unmarshal(raw, &token); err != nil { + return false, "", nil + } + if token.Credentials.SecurityToken == "" && token.Credential.SecurityToken != "" { + token.Credentials = credentials{ + AccessKeyID: token.Credential.Access, + SecretAccessKey: token.Credential.Secret, + SecurityToken: token.Credential.SecurityToken, + Expiration: token.Credential.ExpiresAt, + } + } + if token.Credentials.SecurityToken == "" { + return false, "", nil + } + credential := applyToken(Credential{Format: CredentialFormat, ClientID: ClientID}, &token) + if err := c.saveCredential(ctx, accountID, credential); err != nil { + return false, "", err + } + state.finish(nil) + c.clearLogin(accountID) + return true, "login complete", nil +} + +// currentUser performs a signed identity lookup. It exercises the same STS +// material chat uses, so Probe can treat success as readiness. +func (c *Client) currentUser(ctx context.Context, credential Credential) (uid, name, domain string, err error) { + requestCtx, cancel := context.WithTimeout(ctx, metadataTimeout) + defer cancel() + req, err := http.NewRequestWithContext(requestCtx, http.MethodGet, c.apiBase(credential)+currentUserPath, nil) + if err != nil { + return "", "", "", err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("X-Language", Language) + signRequest(req, nil, credential.Sign()) + client, err := c.httpClient(ctx, "") + if err != nil { + return "", "", "", err + } + resp, err := client.Do(req) + if err != nil { + return "", "", "", err + } + defer resp.Body.Close() + raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if resp.StatusCode >= 400 { + return "", "", "", newProviderError(resp.StatusCode, string(raw)) + } + var out struct { + UserID string `json:"user_id"` + UserName string `json:"user_name"` + DomainID string `json:"domain_id"` + } + if err := json.Unmarshal(raw, &out); err != nil { + return "", "", "", fmt.Errorf("parse codearts current user: %w", err) + } + return out.UserID, out.UserName, out.DomainID, nil +} + +func (c *Client) cleanupLogins() { + c.mu.Lock() + defer c.mu.Unlock() + now := c.now() + for accountID, state := range c.logins { + if now.After(state.expiresAt) { + state.finish(fmt.Errorf("login expired")) + if state.listener != nil { + _ = state.listener.Close() + } + delete(c.logins, accountID) + } + } +} + +func (c *Client) clearLogin(accountID string) { + c.mu.Lock() + state := c.logins[accountID] + delete(c.logins, accountID) + c.mu.Unlock() + if state != nil && state.listener != nil { + _ = state.listener.Close() + } +} + +func pkcePair() (verifier, challenge string, err error) { + raw := make([]byte, 64) + if _, err := rand.Read(raw); err != nil { + return "", "", err + } + verifier = base64.RawURLEncoding.EncodeToString(raw) + sum := sha256.Sum256([]byte(verifier)) + return verifier, base64.RawURLEncoding.EncodeToString(sum[:]), nil +} diff --git a/internal/providers/codearts/payload.go b/internal/providers/codearts/payload.go new file mode 100644 index 00000000..cbcd6ef7 --- /dev/null +++ b/internal/providers/codearts/payload.go @@ -0,0 +1,417 @@ +package codearts + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "strings" + + "github.com/caigee-cmd/cli2api/internal/accounts" + "github.com/caigee-cmd/cli2api/internal/providers" + "github.com/caigee-cmd/cli2api/internal/translate" +) + +// buildChatBody maps the internal request to the native CodeArts +// chat-completions shape. The upstream accepts the OpenAI wire format, with two +// deviations: tool_choice must be a string, and max_tokens is the only cap. +func buildChatBody(req translate.ChatRequest, model, level string, allowImages, benefit bool) (map[string]any, error) { + tools, choice, err := nativeTools(req.Tools, req.ToolChoice) + if err != nil { + return nil, err + } + messages, err := upstreamMessages(req.Messages, model, allowImages) + if err != nil { + return nil, err + } + if len(messages) == 0 { + return nil, fmt.Errorf("codearts request has no messages") + } + body := map[string]any{ + "model": model, + "stream": true, + "messages": messages, + "tool_stream": true, + "prompt_cache_key": chatIDFor(req.Messages), + } + if chatID := chatIDFor(req.Messages); chatID != "" { + body["chat_id"] = chatID + } + if level != "" && level != "none" { + body["reasoning_effort"] = level + } + if value := rawNumber(req.MaxTokens); value != nil { + body["max_tokens"] = value + } + if req.MaxCompletionTokens != nil { + if value := rawNumber(req.MaxCompletionTokens); value != nil { + body["max_tokens"] = value + } + } + if value := rawNumber(req.Temperature); value != nil { + body["temperature"] = value + } + if value := rawNumber(req.TopP); value != nil { + body["top_p"] = value + } + if len(tools) > 0 { + body["tools"] = tools + if choice != "" { + body["tool_choice"] = choice + } + } + _ = benefit // benefit routing is a header, not a body field + return body, nil +} + +// chatIDFor derives a stable upstream chat id from the first user message. The +// V2 endpoint does not keep semantic context (the full message list is replayed +// on every turn); the id only drives prompt-cache affinity. +func chatIDFor(messages []translate.ChatMessage) string { + for _, message := range messages { + if !strings.EqualFold(strings.TrimSpace(message.Role), "user") { + continue + } + text := strings.TrimSpace(translate.ContentToString(message.Content)) + if text == "" { + continue + } + sum := sha256.Sum256([]byte("codearts:" + text)) + return hex.EncodeToString(sum[:16]) + } + return "" +} + +// upstreamMessages maps roles and content, dropping incomplete tool pairs. +// Image parts are forwarded as OpenAI image_url parts for vision models; a +// text-only model rejects them with a 400 instead of silently dropping them. +func upstreamMessages(messages []translate.ChatMessage, model string, allowImages bool) ([]map[string]any, error) { + keepCalls, keepResults := pairedToolMessages(messages) + out := make([]map[string]any, 0, len(messages)) + for index, message := range messages { + role := strings.ToLower(strings.TrimSpace(message.Role)) + if role == "developer" { + role = "system" + } + switch role { + case "system", "user": + content, err := upstreamContent(message.Content, model, allowImages) + if err != nil { + return nil, err + } + if content == nil { + continue + } + out = append(out, map[string]any{"role": role, "content": content}) + case "assistant": + wire := map[string]any{"role": "assistant", "content": translate.ContentToString(message.Content)} + if strings.TrimSpace(message.ReasoningContent) != "" { + wire["reasoning_content"] = message.ReasoningContent + } + if keepCalls[index] && len(message.ToolCalls) > 0 { + if calls := decodeToolCalls(message.ToolCalls); len(calls) > 0 { + wire["tool_calls"] = calls + if strings.TrimSpace(translate.ContentToString(message.Content)) == "" { + wire["content"] = nil + } + } + } + out = append(out, wire) + case "tool": + if !keepResults[index] { + continue + } + content := translate.ContentToString(message.Content) + if strings.TrimSpace(content) == "" { + content = "(no output)" + } + wire := map[string]any{"role": "tool", "content": content, "tool_call_id": message.ToolCallID} + if strings.TrimSpace(message.Name) != "" { + wire["name"] = message.Name + } + out = append(out, wire) + } + } + return out, nil +} + +// upstreamContent keeps the OpenAI content-part shape when an image is +// present. A plain text message stays a string so text-only requests are +// byte-for-byte unchanged. +func upstreamContent(content any, model string, allowImages bool) (any, error) { + text := translate.ContentToString(content) + items, isParts := content.([]any) + if !isParts { + if strings.TrimSpace(text) == "" { + return nil, nil + } + return text, nil + } + parts := make([]any, 0, len(items)) + for _, item := range items { + block, ok := item.(map[string]any) + if !ok { + continue + } + kind := strings.ToLower(strings.TrimSpace(stringValue(block["type"]))) + if kind == "" && block["text"] != nil { + kind = "text" + } + switch kind { + case "text", "input_text": + if value, ok := block["text"].(string); ok && value != "" { + parts = append(parts, map[string]any{"type": "text", "text": value}) + } + case "image_url", "input_image", "image": + if !allowImages { + return nil, &providers.Error{ + Kind: accounts.KindInvalidRequest, + Status: 400, + Message: fmt.Sprintf("codearts model %q does not support image input", model), + } + } + url := imageURLFromPart(block) + if url == "" { + return nil, &providers.Error{ + Kind: accounts.KindInvalidRequest, + Status: 400, + Message: "codearts image part is missing a URL", + } + } + parts = append(parts, map[string]any{ + "type": "image_url", "image_url": map[string]any{"url": url}, + }) + } + } + if len(parts) == 0 { + if strings.TrimSpace(text) == "" { + return nil, nil + } + return text, nil + } + if len(parts) == 1 { + if textPart, ok := parts[0].(map[string]any); ok && textPart["type"] == "text" { + if value, ok := textPart["text"].(string); ok { + return value, nil + } + } + } + return parts, nil +} + +func imageURLFromPart(block map[string]any) string { + for _, key := range []string{"image_url", "image"} { + if value, ok := block[key]; ok { + if url := imageURLValue(value); url != "" { + return url + } + } + } + if source, ok := block["source"].(map[string]any); ok { + if data := stringValue(source["data"]); data != "" { + mime := stringValue(source["media_type"]) + if mime == "" { + mime = "image/png" + } + return "data:" + mime + ";base64," + data + } + } + return stringValue(block["url"]) +} + +func imageURLValue(value any) string { + switch typed := value.(type) { + case string: + return strings.TrimSpace(typed) + case map[string]any: + return stringValue(typed["url"]) + case map[string]string: + return strings.TrimSpace(typed["url"]) + default: + return "" + } +} + +func stringValue(value any) string { + if text, ok := value.(string); ok { + return strings.TrimSpace(text) + } + return "" +} + +// pairedToolMessages keeps an assistant tool_calls block only when every call +// has a matching tool result. CodeArts rejects orphaned tool history with a 400. +func pairedToolMessages(messages []translate.ChatMessage) (map[int]bool, map[int]bool) { + resultIndex := map[string]int{} + for index, message := range messages { + if strings.EqualFold(strings.TrimSpace(message.Role), "tool") && message.ToolCallID != "" { + resultIndex[message.ToolCallID] = index + } + } + keepCalls := map[int]bool{} + keepResults := map[int]bool{} + for index, message := range messages { + if !strings.EqualFold(strings.TrimSpace(message.Role), "assistant") || len(message.ToolCalls) == 0 { + continue + } + calls := decodeToolCalls(message.ToolCalls) + if len(calls) == 0 { + continue + } + complete := true + for _, call := range calls { + id, _ := call["id"].(string) + if id == "" { + complete = false + break + } + if _, ok := resultIndex[id]; !ok { + complete = false + break + } + } + if !complete { + continue + } + keepCalls[index] = true + for _, call := range calls { + id, _ := call["id"].(string) + keepResults[resultIndex[id]] = true + } + } + return keepCalls, keepResults +} + +// decodeToolCalls normalizes arguments to the string form OpenAI clients send. +func decodeToolCalls(raw json.RawMessage) []map[string]any { + if len(raw) == 0 || string(raw) == "null" { + return nil + } + var calls []map[string]any + if err := json.Unmarshal(raw, &calls); err != nil { + return nil + } + out := make([]map[string]any, 0, len(calls)) + for _, call := range calls { + id, _ := call["id"].(string) + callType, _ := call["type"].(string) + if callType == "" { + callType = "function" + } + fn, _ := call["function"].(map[string]any) + name, _ := fn["name"].(string) + if name == "" { + continue + } + arguments := fn["arguments"] + wire := map[string]any{ + "id": id, + "type": callType, + "function": map[string]any{ + "name": name, + "arguments": rawArguments(arguments), + }, + } + out = append(out, wire) + } + return out +} + +func rawArguments(value any) string { + switch typed := value.(type) { + case string: + return typed + case nil: + return "{}" + default: + if raw, err := json.Marshal(typed); err == nil { + return string(raw) + } + return "{}" + } +} + +// nativeTools narrows a named function choice to that one tool and converts the +// choice to the string form CodeArts accepts. +func nativeTools(raw json.RawMessage, choiceRaw json.RawMessage) ([]map[string]any, string, error) { + var tools []map[string]any + if len(raw) > 0 && string(raw) != "null" { + if err := json.Unmarshal(raw, &tools); err != nil { + return nil, "", &providers.Error{Kind: accounts.KindInvalidRequest, Status: 400, Message: "codearts tools must be an array"} + } + } + mode, function := parseToolChoice(choiceRaw) + if mode == "" { + mode = "auto" + } + if mode == "function" { + var selected map[string]any + for _, tool := range tools { + fn, _ := tool["function"].(map[string]any) + if name, _ := fn["name"].(string); name == function { + if selected != nil { + return nil, "", fmt.Errorf("tool_choice function %q is defined more than once", function) + } + selected = tool + } + } + if selected == nil { + return nil, "", fmt.Errorf("tool_choice function %q is not defined in tools", function) + } + tools = []map[string]any{selected} + mode = "required" + } + if len(tools) == 0 { + return nil, "", nil + } + switch mode { + case "none", "required", "auto": + return tools, mode, nil + default: + return tools, "auto", nil + } +} + +func parseToolChoice(raw json.RawMessage) (mode, function string) { + if len(raw) == 0 || string(raw) == "null" { + return "", "" + } + var text string + if json.Unmarshal(raw, &text) == nil { + switch strings.ToLower(strings.TrimSpace(text)) { + case "none", "required", "auto": + return strings.ToLower(strings.TrimSpace(text)), "" + } + return "auto", "" + } + var object struct { + Type string `json:"type"` + Function struct { + Name string `json:"name"` + } `json:"function"` + } + if json.Unmarshal(raw, &object) == nil && object.Function.Name != "" { + return "function", object.Function.Name + } + return "auto", "" +} + +// rawNumber decodes an optional numeric JSON field without losing precision. +func rawNumber(raw json.RawMessage) any { + if len(raw) == 0 || string(raw) == "null" { + return nil + } + decoder := json.NewDecoder(bytes.NewReader(raw)) + decoder.UseNumber() + var value any + if err := decoder.Decode(&value); err != nil { + return nil + } + switch value.(type) { + case json.Number: + return value + default: + return nil + } +} diff --git a/internal/providers/codearts/quota.go b/internal/providers/codearts/quota.go new file mode 100644 index 00000000..1c1265fc --- /dev/null +++ b/internal/providers/codearts/quota.go @@ -0,0 +1,251 @@ +package codearts + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/caigee-cmd/cli2api/internal/providers" +) + +// quotaMeter is one row of the official subscription statistics document. +type quotaMeter struct { + Name string + Label string + UsedTokens int64 + Allowance int64 + CreditTotal float64 + CreditUsed float64 + CreditRemain float64 + Percent float64 + HasPercent bool +} + +// statistics is the parsed /snap-manager/v1/statistics/plugin response. +type statistics struct { + ResetDate string + Plan string + PlanName string + Meters []quotaMeter +} + +func (s statistics) empty() bool { return len(s.Meters) == 0 } + +func (m quotaMeter) unit() string { + if m.CreditTotal > 0 || m.CreditUsed > 0 || m.CreditRemain > 0 { + return "积分" + } + if m.UsedTokens > 0 || m.Allowance > 0 { + return "tokens" + } + return "" +} + +func (m quotaMeter) used() float64 { + if m.CreditTotal > 0 || m.CreditUsed > 0 || m.CreditRemain > 0 { + return m.CreditUsed + } + return float64(m.UsedTokens) +} + +func (m quotaMeter) total() float64 { + if m.CreditTotal > 0 { + return m.CreditTotal + } + // package_token_amount=1 is the upstream's placeholder when the trial + // account has no chat-token package; it is a counter, not a 1-token quota. + if m.Allowance > 1 { + return float64(m.Allowance) + } + return 0 +} + +func (m quotaMeter) remaining() float64 { + if m.CreditRemain > 0 || m.CreditTotal > 0 { + if m.CreditRemain > 0 { + return m.CreditRemain + } + return m.CreditTotal - m.CreditUsed + } + if m.Allowance > 1 { + return float64(m.Allowance - m.UsedTokens) + } + return 0 +} + +func (m quotaMeter) percentage() float64 { + if m.total() <= 0 { + return 0 + } + if m.HasPercent { + return m.Percent + } + return m.used() / m.total() * 100 +} + +// Quota reads the package meters. The activity/benefit balance is intentionally +// not merged: it is a different accounting pool and mixing it would report a +// fake total. +func (c *Client) Quota(ctx context.Context, accountID string) (*providers.QuotaInfo, error) { + credential, err := c.credential(ctx, accountID) + if err != nil { + return nil, err + } + stats, err := c.fetchStatistics(ctx, accountID, credential) + if err != nil { + return nil, err + } + if stats.empty() { + return nil, fmt.Errorf("codearts statistics returned no visible quota meters") + } + info := &providers.QuotaInfo{ + ProviderID: ProviderID, + Plan: stats.PlanName, + FetchedAt: c.now().Format(time.RFC3339), + } + for _, meter := range stats.Meters { + window := providers.QuotaWindow{ + ID: meter.Name, + Label: meter.Label, + Used: meter.used(), + Total: meter.total(), + Remaining: meter.remaining(), + Percentage: meter.percentage(), + Unit: meter.unit(), + ResetAt: stats.ResetDate, + Exceeded: meter.remaining() <= 0 && meter.total() > 0, + } + info.Windows = append(info.Windows, window) + } + // The package-credit meter is the plan-level number; fall back to the first + // visible meter when a plan does not expose it. + primary := info.Windows[0] + for _, window := range info.Windows { + if window.ID == "usageTotalPackageCredit" { + primary = window + break + } + } + info.Used = primary.Used + info.Total = primary.Total + info.Remaining = primary.Remaining + info.Percentage = primary.Percentage + info.Unit = primary.Unit + info.Exceeded = primary.Exceeded + return info, nil +} + +func (c *Client) fetchStatistics(ctx context.Context, accountID string, credential Credential) (statistics, error) { + requestCtx, cancel := context.WithTimeout(ctx, metadataTimeout) + defer cancel() + req, err := http.NewRequestWithContext(requestCtx, http.MethodGet, c.apiBase(credential)+statisticsPath, nil) + if err != nil { + return statistics{}, err + } + // The statistics endpoint signs this exact header set; do not add + // Content-Type or Agent-Type, which would change the signature. + req.Header.Set("Accept", "application/json") + req.Header.Set("X-Language", Language) + req.Header.Set("plugin-name", PluginName) + req.Header.Set("plugin-version", PluginVersion) + signRequest(req, nil, credential.Sign()) + client, err := c.httpClient(ctx, accountID) + if err != nil { + return statistics{}, err + } + resp, err := client.Do(req) + if err != nil { + return statistics{}, err + } + defer resp.Body.Close() + raw, _ := io.ReadAll(io.LimitReader(resp.Body, 2<<20)) + if resp.StatusCode >= 400 { + return statistics{}, newProviderError(resp.StatusCode, string(raw)) + } + return parseStatistics(raw) +} + +func parseStatistics(raw []byte) (statistics, error) { + var parsed struct { + EndDate string `json:"end_date"` + Metrics []struct { + Name string `json:"name"` + Show bool `json:"show"` + UsedTokens int64 `json:"usage_token_num"` + Allowance int64 `json:"package_token_amount"` + CreditTotal float64 `json:"package_credit_amount"` + CreditUsed float64 `json:"package_credit_used"` + CreditRemain float64 `json:"package_credit_remain"` + Value *float64 `json:"value"` + } `json:"metrics"` + Package *struct { + SpecCode string `json:"spec_code"` + PackageNameEN string `json:"package_name_en"` + PackageNameCN string `json:"package_name_cn"` + } `json:"package"` + } + if err := json.Unmarshal(raw, &parsed); err != nil { + return statistics{}, fmt.Errorf("parse codearts statistics: %w", err) + } + out := statistics{ResetDate: strings.TrimSpace(parsed.EndDate)} + if parsed.Package != nil { + out.Plan = strings.TrimSpace(parsed.Package.SpecCode) + out.PlanName = firstNonEmpty(parsed.Package.PackageNameEN, parsed.Package.PackageNameCN) + } + for _, metric := range parsed.Metrics { + name := strings.TrimSpace(metric.Name) + if name == "" || !metric.Show { + continue + } + meter := quotaMeter{ + Name: name, + Label: quotaMeterLabel(name), + UsedTokens: metric.UsedTokens, + Allowance: metric.Allowance, + CreditTotal: metric.CreditTotal, + CreditUsed: metric.CreditUsed, + CreditRemain: metric.CreditRemain, + } + if metric.Value != nil && *metric.Value >= 0 { + meter.Percent = *metric.Value + meter.HasPercent = true + } + if meter.Label == "" { + meter.Label = name + } + if meter.UsedTokens == 0 && meter.Allowance <= 1 && + meter.CreditTotal == 0 && meter.CreditUsed == 0 && meter.CreditRemain == 0 { + // A zero-credit row or the placeholder 1-token counter is noise. + continue + } + out.Meters = append(out.Meters, meter) + } + return out, nil +} + +// quotaMeterLabel is copied from the verified upstream names; unknown names +// keep their raw id rather than being hidden. +func quotaMeterLabel(name string) string { + switch name { + case "usageDataCodeCompletions": + return "代码补全额度" + case "usageDataChatMessages": + return "对话消息额度" + case "usageTokenChatMessages": + return "对话 token 额度" + case "usageTotalPackageCredit": + return "套餐积分" + case "usageBasicPackageCredit": + return "基础包积分" + case "usageOnDemandPackageCredit": + return "按需付费积分" + case "usageBonusPackageCredit": + return "赠送积分" + default: + return "" + } +} diff --git a/internal/providers/codearts/s04_interfaces_test.go b/internal/providers/codearts/s04_interfaces_test.go new file mode 100644 index 00000000..3b40f6b1 --- /dev/null +++ b/internal/providers/codearts/s04_interfaces_test.go @@ -0,0 +1,60 @@ +package codearts + +import ( + "context" + "testing" + + "github.com/caigee-cmd/cli2api/internal/accounts" + "github.com/caigee-cmd/cli2api/internal/providers" +) + +// requiredStore is the minimum store surface every in-process adapter needs. +type requiredStore struct{} + +func (requiredStore) Get(context.Context, string) (accounts.Account, error) { + return accounts.Account{}, nil +} +func (requiredStore) LoadCredentialPayload(context.Context, string) (string, []byte, error) { + return "", nil, accounts.ErrAccountNotFound +} +func (requiredStore) SaveCredentialPayload(context.Context, string, string, []byte) error { + return nil +} +func (requiredStore) Observe(context.Context, string, string, string, string, string) error { + return nil +} + +func TestS04InterfaceContract(t *testing.T) { + client := NewClient(requiredStore{}) + var _ providers.CredentialCodec = credentialCodec{} + var _ providers.LoginSessionProvider = client + var _ providers.LoginCompleter = client + var _ providers.ProviderChat = client + var _ providers.ModelCatalogProvider = client + var _ providers.AccountProber = client + var _ providers.ErrorClassifier = classifier{} + var _ providers.CredentialImporter = importer{} + var _ providers.ImportExporter = importer{} + var _ providers.AccountCheckiner = client + + adapter := client.Adapter() + if adapter.ID != ProviderID { + t.Fatalf("adapter id = %q", adapter.ID) + } + for _, capability := range []string{"credential", "login", "chat", "models", "classifier", "import_export", "prober", "checkin"} { + if !adapter.Supports(capability) { + t.Fatalf("adapter missing capability %q", capability) + } + } +} + +func TestMissingSecretReaderSkipsGlobalProxy(t *testing.T) { + client := NewClient(requiredStore{}) + got, err := client.globalProxy(context.Background()) + if err != nil { + t.Fatalf("missing SecretReader must skip, not fail: %v", err) + } + if got != "" { + t.Fatalf("missing SecretReader returned %q", got) + } +} diff --git a/internal/providers/codearts/signer.go b/internal/providers/codearts/signer.go new file mode 100644 index 00000000..0ece20eb --- /dev/null +++ b/internal/providers/codearts/signer.go @@ -0,0 +1,120 @@ +package codearts + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "net/http" + "net/url" + "sort" + "strings" + "time" +) + +const signingAlgorithm = "SDK-HMAC-SHA256" + +// SignCredential is the short-lived STS trio used to sign every gateway call. +type SignCredential struct { + AccessKeyID string + SecretAccessKey string + SecurityToken string +} + +// signRequest adds the Huawei Cloud SDK-HMAC-SHA256 headers. The upstream +// verifies the exact header set, so this must stay byte-for-byte compatible +// with the official signer. +func signRequest(req *http.Request, body []byte, cred SignCredential) { + xDate := time.Now().UTC().Format("20060102T150405Z") + req.Header.Set("X-Sdk-Date", xDate) + req.Header.Set("Host", req.URL.Host) + if cred.SecurityToken != "" { + req.Header.Set("X-Security-Token", cred.SecurityToken) + } + payloadHash := sha256Hex(body) + req.Header.Set("X-Sdk-Content-Sha256", payloadHash) + + canonicalHeaders, signedHeaders := canonicalHeaders(req) + canonicalRequest := strings.Join([]string{ + req.Method, + canonicalURI(req.URL.EscapedPath()), + canonicalQuery(req.URL.RawQuery), + canonicalHeaders, + signedHeaders, + payloadHash, + }, "\n") + stringToSign := strings.Join([]string{ + signingAlgorithm, + xDate, + sha256Hex([]byte(canonicalRequest)), + }, "\n") + signature := hmacHex(cred.SecretAccessKey, stringToSign) + req.Header.Set("Authorization", signingAlgorithm+ + " Access="+cred.AccessKeyID+ + ", SignedHeaders="+signedHeaders+ + ", Signature="+signature) +} + +// canonicalHeaders signs every request header (lower-case keys, sorted), as the +// official Java/JS SDKs do. +func canonicalHeaders(req *http.Request) (headers, signed string) { + keys := make([]string, 0, len(req.Header)) + for k := range req.Header { + keys = append(keys, strings.ToLower(k)) + } + sort.Strings(keys) + var sb strings.Builder + for _, k := range keys { + v := req.Header.Get(k) + sb.WriteString(k + ":" + strings.TrimSpace(v) + "\n") + } + return sb.String(), strings.Join(keys, ";") +} + +// canonicalURI percent-encodes each segment and appends a trailing slash. +func canonicalURI(path string) string { + segments := strings.Split(path, "/") + for i, seg := range segments { + segments[i] = url.PathEscape(seg) + } + out := strings.Join(segments, "/") + if !strings.HasSuffix(out, "/") { + out += "/" + } + return out +} + +// canonicalQuery sorts keys and values. +func canonicalQuery(raw string) string { + if raw == "" { + return "" + } + vals, err := url.ParseQuery(raw) + if err != nil { + return raw + } + keys := make([]string, 0, len(vals)) + for k := range vals { + keys = append(keys, k) + } + sort.Strings(keys) + parts := make([]string, 0, len(keys)) + for _, k := range keys { + vs := vals[k] + sort.Strings(vs) + for _, v := range vs { + parts = append(parts, url.QueryEscape(k)+"="+url.QueryEscape(v)) + } + } + return strings.Join(parts, "&") +} + +func sha256Hex(b []byte) string { + sum := sha256.Sum256(b) + return hex.EncodeToString(sum[:]) +} + +func hmacHex(key, msg string) string { + mac := hmac.New(sha256.New, []byte(key)) + _, _ = mac.Write([]byte(msg)) + return hex.EncodeToString(mac.Sum(nil)) +} diff --git a/internal/providers/codearts/sse.go b/internal/providers/codearts/sse.go new file mode 100644 index 00000000..15f36e34 --- /dev/null +++ b/internal/providers/codearts/sse.go @@ -0,0 +1,462 @@ +package codearts + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "sort" + "strings" + "time" + + "github.com/caigee-cmd/cli2api/internal/providers" +) + +// upstreamError is a business error embedded in an HTTP 200 SSE frame. +type upstreamError struct { + Code string + Msg string +} + +func (e *upstreamError) Error() string { + if e.Code != "" { + return fmt.Sprintf("codearts error code=%s msg=%s", e.Code, e.Msg) + } + return "codearts error: " + e.Msg +} + +// rawCompletion is the aggregated non-stream result. +type rawCompletion struct { + Content string + Reasoning string + Finish string + ToolCalls []map[string]any + Usage map[string]any +} + +// scanLine parses one SSE line. CodeArts usually sends one data: line per +// event without a blank separator; both that and the standard form are +// accepted. +func scanLine(line string, pendingEvent *string) (event, data string, ok bool) { + line = strings.TrimRight(line, "\r") + switch { + case strings.HasPrefix(line, "event:"): + *pendingEvent = strings.TrimSpace(strings.TrimPrefix(line, "event:")) + return "", "", false + case strings.HasPrefix(line, "data:"): + value := strings.TrimSpace(strings.TrimPrefix(line, "data:")) + if value == "" { + return "", "", false + } + event = *pendingEvent + *pendingEvent = "" + return event, value, true + case strings.HasPrefix(line, ":"), line == "": + *pendingEvent = "" + return "", "", false + } + return "", "", false +} + +// deltaFromChunk extracts a standard OpenAI delta. +func deltaFromChunk(payload map[string]any) (delta map[string]any, finishReason string) { + if direct, ok := payload["delta"].(map[string]any); ok { + return direct, "" + } + choices, ok := payload["choices"].([]any) + if !ok || len(choices) == 0 { + return nil, "" + } + return choiceDelta(choices[0]) +} + +func choiceDelta(raw any) (map[string]any, string) { + choice, ok := raw.(map[string]any) + if !ok { + return nil, "" + } + finish, _ := choice["finish_reason"].(string) + delta, _ := choice["delta"].(map[string]any) + return delta, finish +} + +// applyToolCallDeltas merges streamed tool-call fragments by index. +func applyToolCallDeltas(delta map[string]any, calls map[int]map[string]any) { + rawCalls, ok := delta["tool_calls"].([]any) + if !ok { + return + } + for position, raw := range rawCalls { + entry, ok := raw.(map[string]any) + if !ok { + continue + } + index := position + switch value := entry["index"].(type) { + case float64: + index = int(value) + case json.Number: + if parsed, err := value.Int64(); err == nil { + index = int(parsed) + } + } + call := calls[index] + if call == nil { + call = map[string]any{"index": index, "type": "function"} + calls[index] = call + } + if id, ok := entry["id"].(string); ok && id != "" { + call["id"] = id + } + if typ, ok := entry["type"].(string); ok && typ != "" { + call["type"] = typ + } + if fn, ok := entry["function"].(map[string]any); ok { + current, _ := call["function"].(map[string]any) + if current == nil { + current = map[string]any{"name": "", "arguments": ""} + call["function"] = current + } + if name, ok := fn["name"].(string); ok { + current["name"] = current["name"].(string) + name + } + if arguments, ok := fn["arguments"].(string); ok { + current["arguments"] = current["arguments"].(string) + arguments + } + } + } +} + +func sortedToolCalls(calls map[int]map[string]any) []map[string]any { + indexes := make([]int, 0, len(calls)) + for index := range calls { + indexes = append(indexes, index) + } + sort.Ints(indexes) + out := make([]map[string]any, 0, len(indexes)) + for _, index := range indexes { + out = append(out, calls[index]) + } + return out +} + +// applyEvent updates the aggregate from one SSE frame. +func applyEvent(content, reasoning *strings.Builder, finish *string, upErr *error, event, data string) { + event = strings.ToLower(strings.TrimSpace(event)) + var payload map[string]any + if strings.TrimSpace(data) != "" { + _ = json.Unmarshal([]byte(data), &payload) + } + text := func(keys ...string) string { + for _, key := range keys { + if value, ok := payload[key].(string); ok { + return value + } + } + return "" + } + code := text("error_code", "code") + if code != "" && code != "0" && code != "0000" { + *upErr = &upstreamError{Code: code, Msg: firstNonEmpty(text("error_msg", "message", "msg"), "request failed")} + return + } + switch event { + case "", "data", "onanswer", "answer", "delta", "message", "content": + if strings.TrimSpace(data) == "[DONE]" { + if *finish == "" { + *finish = "stop" + } + return + } + delta, finishReason := deltaFromChunk(payload) + if finishReason != "" { + *finish = finishReason + } + if delta != nil { + if value, ok := delta["content"].(string); ok && value != "" { + content.WriteString(value) + } + if value, ok := delta["reasoning_content"].(string); ok && value != "" { + reasoning.WriteString(value) + } + return + } + if t := text("text", "content", "delta"); t != "" { + if strings.EqualFold(t, "done") || t == "[DONE]" { + *finish = "stop" + return + } + // CodeArts' text field is a full snapshot, not a delta. + content.Reset() + content.WriteString(t) + return + } + if out, ok := payload["output"].([]any); ok && len(out) > 0 { + var sb strings.Builder + for _, item := range out { + if object, ok := item.(map[string]any); ok && object["type"] == "output_text" { + if value, ok := object["text"].(string); ok { + sb.WriteString(value) + } + } + } + if sb.Len() > 0 { + content.Reset() + content.WriteString(sb.String()) + } + } + case "reasoning", "thinking", "onreasoning", "onthinking": + if value := text("text", "reasoning", "content", "thinking"); value != "" { + reasoning.WriteString(value) + } + case "done", "end", "finish": + if value := text("finish_reason", "finish", "reason"); value != "" { + *finish = value + } else if *finish == "" { + *finish = "stop" + } + } +} + +// aggregateRaw reads a complete SSE stream and returns text, reasoning, tool +// calls and usage. It is the non-stream path. +func aggregateRaw(reader io.Reader) (*rawCompletion, error) { + buffered := bufio.NewReaderSize(reader, 64*1024) + content := &strings.Builder{} + reasoning := &strings.Builder{} + finish := "stop" + calls := map[int]map[string]any{} + var usage map[string]any + var streamErr error + var pendingEvent string + for { + line, err := buffered.ReadString('\n') + if err != nil && err != io.EOF { + return nil, err + } + if event, data, ok := scanLine(strings.TrimRight(line, "\r\n"), &pendingEvent); ok { + var payload map[string]any + if json.Unmarshal([]byte(data), &payload) == nil { + if value, ok := payload["usage"].(map[string]any); ok { + usage = value + } + delta, _ := deltaFromChunk(payload) + applyToolCallDeltas(delta, calls) + } + applyEvent(content, reasoning, &finish, &streamErr, event, data) + } + if err == io.EOF { + break + } + } + if streamErr != nil { + if embedded, ok := streamErr.(*upstreamError); ok { + return nil, newProviderError(http.StatusBadGateway, embedded.Error()) + } + return nil, streamErr + } + return &rawCompletion{ + Content: content.String(), + Reasoning: reasoning.String(), + Finish: finish, + ToolCalls: sortedToolCalls(calls), + Usage: usage, + }, nil +} + +// streamCapture rewrites upstream SSE to standard chat-completions chunks. +func streamCapture(writer io.Writer, reader io.Reader, model string) error { + buffered := bufio.NewReaderSize(reader, 64*1024) + id := fmt.Sprintf("chatcmpl-%d", time.Now().UnixNano()) + content := &strings.Builder{} + reasoning := &strings.Builder{} + finish := "stop" + calls := map[int]map[string]any{} + var usage map[string]any + var pendingEvent string + sawDone := false + var streamErr error + + write := func(payload map[string]any) error { + raw, _ := json.Marshal(payload) + if _, err := io.WriteString(writer, "data: "+string(raw)+"\n\n"); err != nil { + return err + } + return nil + } + writeChunk := func(delta map[string]any, finishReason string) error { + choice := map[string]any{"index": 0, "delta": delta} + if finishReason != "" { + choice["finish_reason"] = finishReason + } + return write(map[string]any{ + "id": id, "object": "chat.completion.chunk", "created": time.Now().Unix(), + "model": model, "choices": []any{choice}, + }) + } + writeDONE := func() error { + if usage != nil { + if err := write(map[string]any{ + "id": id, "object": "chat.completion.chunk", "created": time.Now().Unix(), + "model": model, "choices": []any{}, "usage": usage, + }); err != nil { + return err + } + } + _, err := io.WriteString(writer, "data: [DONE]\n\n") + return err + } + writeError := func(code, message string) error { + raw, _ := json.Marshal(map[string]any{"error": map[string]any{ + "message": message, "type": "upstream_error", "code": code, + }}) + _, err := io.WriteString(writer, "event: error\ndata: "+string(raw)+"\n\n") + return err + } + + for { + line, err := buffered.ReadString('\n') + if err != nil && err != io.EOF { + streamErr = err + break + } + if event, data, ok := scanLine(strings.TrimRight(line, "\r\n"), &pendingEvent); ok { + if strings.TrimSpace(data) == "[DONE]" { + if !sawDone { + if err := writeDONE(); err != nil { + streamErr = err + break + } + sawDone = true + } + break + } + var payload map[string]any + _ = json.Unmarshal([]byte(data), &payload) + if value, ok := payload["usage"].(map[string]any); ok { + usage = value + } + nativeDelta, nativeFinish := deltaFromChunk(payload) + applyToolCallDeltas(nativeDelta, calls) + beforeContent, beforeReason := content.Len(), reasoning.Len() + var upErr error + applyEvent(content, reasoning, &finish, &upErr, event, data) + if upErr != nil { + classified := newProviderError(http.StatusBadGateway, upErr.Error()) + if err := writeError(classified.Kind, classified.Message); err != nil { + streamErr = err + break + } + _ = writeDONE() + sawDone = true + streamErr = classified + break + } + delta := map[string]any{} + if role, ok := nativeDelta["role"].(string); ok && role != "" { + delta["role"] = role + } + if content.Len() > beforeContent { + delta["content"] = content.String()[beforeContent:] + } + if reasoning.Len() > beforeReason { + delta["reasoning_content"] = reasoning.String()[beforeReason:] + } + if toolCalls, ok := nativeDelta["tool_calls"]; ok { + delta["tool_calls"] = toolCalls + } + if len(delta) > 0 || nativeFinish != "" { + if err := writeChunk(delta, nativeFinish); err != nil { + streamErr = err + break + } + } + if strings.EqualFold(event, "done") || strings.EqualFold(event, "end") || strings.EqualFold(event, "finish") { + if nativeFinish == "" { + if err := writeChunk(map[string]any{}, finish); err != nil { + streamErr = err + break + } + } + if err := writeDONE(); err != nil { + streamErr = err + break + } + sawDone = true + } + } + if sawDone { + break + } + if err == io.EOF { + break + } + } + if streamErr == nil && !sawDone { + streamErr = writeDONE() + } + return streamErr +} + +// preflightWithTimeout bounds the wait for the first SSE frame. A stuck +// upstream must surface as ErrFirstByteTimeout, not as a context deadline, so +// the executor fails over instead of treating it as a client cancellation. +func preflightWithTimeout(resp *http.Response) (io.ReadCloser, error) { + type result struct { + reader io.ReadCloser + err error + } + done := make(chan result, 1) + go func() { + reader, err := preflightSSE(resp.Body) + done <- result{reader: reader, err: err} + }() + select { + case outcome := <-done: + return outcome.reader, outcome.err + case <-time.After(firstByteTimeout): + _ = resp.Body.Close() + return nil, providers.ErrFirstByteTimeout + } +} + +// preflightSSE waits for the first data frame so a 200 with an embedded +// business error is returned as a provider error before the gateway commits a +// 200 response. It also gives ChatStream a place to enforce TTFB. +func preflightSSE(body io.ReadCloser) (io.ReadCloser, error) { + buffered := bufio.NewReaderSize(body, 1<<20) + var prefix bytes.Buffer + for prefix.Len() < 2<<20 { + line, err := buffered.ReadString('\n') + prefix.WriteString(line) + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(trimmed, "data:") { + data := strings.TrimSpace(strings.TrimPrefix(trimmed, "data:")) + if data != "" && data != "[DONE]" { + var payload map[string]any + if json.Unmarshal([]byte(data), &payload) == nil { + if code, _ := payload["error_code"].(string); code != "" && code != "0" && code != "0000" { + _ = body.Close() + return nil, newProviderError(http.StatusBadGateway, data) + } + } + } + return &replayReadCloser{Reader: io.MultiReader(bytes.NewReader(prefix.Bytes()), buffered), Closer: body}, nil + } + if err != nil { + if err != io.EOF { + _ = body.Close() + return nil, err + } + return &replayReadCloser{Reader: bytes.NewReader(prefix.Bytes()), Closer: body}, nil + } + } + return &replayReadCloser{Reader: io.MultiReader(bytes.NewReader(prefix.Bytes()), buffered), Closer: body}, nil +} + +type replayReadCloser struct { + io.Reader + io.Closer +} diff --git a/internal/providers/registry.go b/internal/providers/registry.go index 480b3139..4ce23638 100644 --- a/internal/providers/registry.go +++ b/internal/providers/registry.go @@ -323,6 +323,34 @@ var Kimi = ProviderDescriptor{ DefaultRegion: "cn", } +// CodeArts descriptor. Protocol constants stay in internal/providers/codearts. +// CN-only in-process adapter for Huawei Cloud CodeArts Agent (码道): OAuth PKCE +// against the CodeArts portal mints a short-lived STS trio, and chat goes +// directly to the snap-access OpenAI-compatible gateway. The AK/SK in the +// credential are the STS material, not the account-level IAM keys. +var CodeArts = ProviderDescriptor{ + ID: "codearts", + Label: "CodeArts", + Runtime: RuntimeInProcess, + AuthTypes: []AuthType{AuthOAuth}, + CredentialFormats: []string{"codearts-oauth-v1"}, + Capabilities: ProviderCapabilities{ + Chat: true, Stream: true, Tools: true, Images: true, Reasoning: true, + ModelCatalog: true, Usage: true, Login: true, BrowserLogin: true, + PATLogin: false, ImportExport: true, + }, + Regions: []RegionDescriptor{ + { + ID: "cn-north-4", Label: "CN", + ChatBase: "https://snap-access.cn-north-4.myhuaweicloud.com", + BillingBase: "https://snap-access.cn-north-4.myhuaweicloud.com", + AuthBase: "https://codearts.huaweicloud.com", + DefaultDomain: "huaweicloud.com", + Checkin: &CheckinPolicy{Timezone: "Asia/Shanghai"}, + }, + }, + DefaultRegion: "cn-north-4", +} // Meta descriptor. Protocol constants stay in internal/providers/meta. // Global-only Muse (Meta) adapter: RFC 8628 device grant against auth.meta.com, @@ -359,6 +387,7 @@ var registry = map[string]ProviderDescriptor{ Zhipu.ID: Zhipu, QwenWork.ID: QwenWork, Kimi.ID: Kimi, + CodeArts.ID: CodeArts, Meta.ID: Meta, } @@ -372,7 +401,7 @@ func Get(id string) (ProviderDescriptor, bool) { } func List() []ProviderDescriptor { - return []ProviderDescriptor{Qoder, WorkBuddy, Trae, Devin, Command, Codex, Zhipu, QwenWork, Kimi, Meta} + return []ProviderDescriptor{Qoder, WorkBuddy, Trae, Devin, Command, Codex, Zhipu, QwenWork, Kimi, CodeArts, Meta} } // Resolve validates a provider/region pair. Empty values fall back to the diff --git a/internal/webui/static/assets/CartesianGrid-CCDEyja0.js b/internal/webui/static/assets/CartesianGrid-7x6C3-nK.js similarity index 99% rename from internal/webui/static/assets/CartesianGrid-CCDEyja0.js rename to internal/webui/static/assets/CartesianGrid-7x6C3-nK.js index dcfc2ebd..2b254406 100644 --- a/internal/webui/static/assets/CartesianGrid-CCDEyja0.js +++ b/internal/webui/static/assets/CartesianGrid-7x6C3-nK.js @@ -1,4 +1,4 @@ -import{$t as e,A as t,At as n,Bt as r,Cn as i,Ct as a,Dt as o,E as s,Et as c,F as l,Ft as u,Gt as d,H as f,Ht as p,I as m,It as h,J as g,Jt as _,K as v,Kt as y,L as b,Lt as x,M as S,Mt as C,N as w,Nt as T,O as E,Ot as D,P as O,Pt as k,R as ee,Rt as te,Sn as A,St as j,Tn as M,Tt as ne,Ut as re,Vt as ie,W as N,Wt as ae,X as oe,Xt as se,Y as P,Yt as ce,Z as le,Zt as ue,an as F,bt as de,cn as fe,ct as I,dn as pe,dt as me,en as L,et as he,fn as ge,gt as _e,ht as ve,it as ye,j as be,jt as xe,k as Se,kt as Ce,l as we,ln as Te,nn as Ee,nt as De,on as Oe,ot as ke,pn as Ae,q as R,qt as je,rt as Me,sn as Ne,st as Pe,tn as z,tt as Fe,u as Ie,ut as Le,vt as Re,wn as ze,wt as Be,xt as Ve,yt as B,z as He,zt as Ue}from"./index-0LMr5JvV.js";function We(e,t){let n=new Map;for(let r=0;rvoid 0)}function et(e,t,n,r,i=!1){if(t===e)return!0;switch(typeof t){case`object`:return tt(e,t,n,r,i);case`function`:return Object.keys(t).length>0?et(e,{...t},n,r,i):j(e,t);default:return Ve(e)&&i?typeof t!=`string`||t===``:j(e,t)}}function tt(e,t,n,r,i=!1){if(t==null)return!0;if(Array.isArray(t))return rt(e,t,n,r);if(t instanceof Map)return nt(e,t,n,r);if(t instanceof Set)return it(e,t,n,r);let a=Object.keys(t);if(e==null)return i&&a.length===0;if(i)Je(e)&&(e=Object(e));else{let t=y(e);if(t!==`[object Object]`&&t!==`[object Arguments]`)return!1}if(a.length===0)return!0;if(r?.has(t))return r.get(t)===e;r?.set(t,e);try{for(let i=0;ivoid 0)}function ot(e){return e=Qe(e),t=>at(t,e)}function st(e,t){return Xe(e,(n,i,a,s)=>{let c=t?.(n,i,a,s);if(c!==void 0)return c;if(typeof e==`object`){if(y(e)===`[object Object]`&&typeof e.constructor!=`function`){let t={};return s.set(e,t),H(t,e,a,s),t}switch(Object.prototype.toString.call(e)){case h:case r:case o:{let t=new e.constructor(e?.valueOf());return H(t,e),t}case Be:{let t={};return H(t,e),t.length=e.length,t[Symbol.iterator]=e[Symbol.iterator],t}default:return}}})}function ct(e){return st(e)}function lt(e){return typeof e==`object`&&!!e&&y(e)===`[object Arguments]`}function ut(e,t){let n;if(n=Array.isArray(t)?t:typeof t==`string`&&Te(t)&&!(t in Object(e))?Ne(t):[t],n.length===0)return!1;let r=e;for(let e=0;evoid 0)}function et(e,t,n,r,i=!1){if(t===e)return!0;switch(typeof t){case`object`:return tt(e,t,n,r,i);case`function`:return Object.keys(t).length>0?et(e,{...t},n,r,i):j(e,t);default:return Ve(e)&&i?typeof t!=`string`||t===``:j(e,t)}}function tt(e,t,n,r,i=!1){if(t==null)return!0;if(Array.isArray(t))return rt(e,t,n,r);if(t instanceof Map)return nt(e,t,n,r);if(t instanceof Set)return it(e,t,n,r);let a=Object.keys(t);if(e==null)return i&&a.length===0;if(i)Je(e)&&(e=Object(e));else{let t=y(e);if(t!==`[object Object]`&&t!==`[object Arguments]`)return!1}if(a.length===0)return!0;if(r?.has(t))return r.get(t)===e;r?.set(t,e);try{for(let i=0;ivoid 0)}function ot(e){return e=Qe(e),t=>at(t,e)}function st(e,t){return Xe(e,(n,i,a,s)=>{let c=t?.(n,i,a,s);if(c!==void 0)return c;if(typeof e==`object`){if(y(e)===`[object Object]`&&typeof e.constructor!=`function`){let t={};return s.set(e,t),H(t,e,a,s),t}switch(Object.prototype.toString.call(e)){case h:case r:case o:{let t=new e.constructor(e?.valueOf());return H(t,e),t}case Be:{let t={};return H(t,e),t.length=e.length,t[Symbol.iterator]=e[Symbol.iterator],t}default:return}}})}function ct(e){return st(e)}function lt(e){return typeof e==`object`&&!!e&&y(e)===`[object Arguments]`}function ut(e,t){let n;if(n=Array.isArray(t)?t:typeof t==`string`&&Te(t)&&!(t in Object(e))?Ne(t):[t],n.length===0)return!1;let r=e;for(let e=0;ee.length)&&(t=e.length);for(var n=0,r=Array(t);nW||Math.abs(e.left-t.left)>W||Math.abs(e.top-t.top)>W||Math.abs(e.width-t.width)>W}function wt(e){var t=e.getBoundingClientRect();return{height:t.height,left:t.left,top:t.top,width:t.width}}function Tt(){var e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:[],t=_t((0,U.useState)({height:0,left:0,top:0,width:0}),2),n=t[0],r=t[1],i=(0,U.useRef)(null),a=(0,U.useRef)(n);a.current=n;var o=(0,U.useCallback)(e=>{if(i.current!=null&&(i.current.disconnect(),i.current=null),e!=null){var t=wt(e);if(Ct(t,a.current)&&r(t),typeof ResizeObserver<`u`){var n=new ResizeObserver(()=>{var t=wt(e);Ct(t,a.current)&&r(t)});n.observe(e),i.current=n}}},[...e]);return(0,U.useEffect)(()=>()=>{var e;(e=i.current)==null||e.disconnect()},[]),[n,o]}function Et(){return Et=Object.assign?Object.assign.bind():function(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=Array(t);n{var n=t.separator,r=n===void 0?K.separator:n,i=t.contentStyle,a=t.itemStyle,o=t.labelStyle,s=o===void 0?K.labelStyle:o,c=t.payload,l=t.formatter,u=t.itemSorter,d=t.wrapperClassName,f=t.labelClassName,p=t.label,m=t.labelFormatter,h=t.accessibilityLayer,g=h===void 0?K.accessibilityLayer:h,_=()=>{if(c&&c.length){var e={padding:0,margin:0},t=Rt(c,u).map((e,t)=>{if(!e||e.type===`none`)return null;var n=e.formatter||l||Lt,i=e.value,o=e.name,s=i,u=o;if(n){var d=n(i,o,e,t,c);if(Array.isArray(d)){var f=jt(d,2);s=f[0],u=f[1]}else if(d!=null)s=d;else return null}var p=G(G({},K.itemStyle),{},{color:e.color||K.itemStyle.color},a);return U.createElement(`li`,{className:`recharts-tooltip-item`,key:`tooltip-item-${t}`,style:p},L(u)?U.createElement(`span`,{className:`recharts-tooltip-item-name`},u):null,L(u)?U.createElement(`span`,{className:`recharts-tooltip-item-separator`},r):null,U.createElement(`span`,{className:`recharts-tooltip-item-value`},s),U.createElement(`span`,{className:`recharts-tooltip-item-unit`},e.unit||``))});return U.createElement(`ul`,{className:`recharts-tooltip-item-list`,style:e},t)}return null},v=G(G({},K.contentStyle),i),y=G({margin:0},s),b=!e(p),x=b?p:``,S=A(`recharts-default-tooltip`,d),C=A(`recharts-tooltip-label`,f);b&&m&&c!=null&&(x=m(p,c));var w=g?{role:`status`,"aria-live":`assertive`}:{};return U.createElement(`div`,Et({className:S,style:v},w),U.createElement(`p`,{className:C,style:y},U.isValidElement(x)?x:`${x}`),_())},q=`recharts-tooltip-wrapper`,Bt={visibility:`hidden`};function Vt(e){var t=e.coordinate,n=e.translateX,r=e.translateY;return A(q,{[`${q}-right`]:z(n)&&t&&z(t.x)&&n>=t.x,[`${q}-left`]:z(n)&&t&&z(t.x)&&n=t.y,[`${q}-top`]:z(r)&&t&&z(t.y)&&r0?i:0),d=n[r]+i;if(t[r])return o[r]?u:d;var f=c[r];return f==null?0:o[r]?Math.max(uf+l?Math.max(u,f):Math.max(d,f)}function Ut(e){var t=e.translateX,n=e.translateY;return{transform:e.useTranslate3d?`translate3d(${t}px, ${n}px, 0)`:`translate(${t}px, ${n}px)`}}function Wt(e){var t=e.allowEscapeViewBox,n=e.coordinate,r=e.offsetTop,i=e.offsetLeft,a=e.position,o=e.reverseDirection,s=e.tooltipBox,c=e.useTranslate3d,l=e.viewBox,u,d,f;return s&&s.height>0&&s.width>0&&n?(d=Ht({allowEscapeViewBox:t,coordinate:n,key:`x`,offset:i,position:a,reverseDirection:o,tooltipDimension:s.width,viewBox:l,viewBoxDimension:l.width}),f=Ht({allowEscapeViewBox:t,coordinate:n,key:`y`,offset:r,position:a,reverseDirection:o,tooltipDimension:s.height,viewBox:l,viewBoxDimension:l.height}),u=Ut({translateX:d,translateY:f,useTranslate3d:c})):u=Bt,{cssProperties:u,cssClasses:Vt({translateX:d,translateY:f,coordinate:n})}}function Gt(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),n.push.apply(n,r)}return n}function J(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=Array(t);n({dismissed:!1,dismissedAtCoordinate:{x:0,y:0}})),2),r=n[0],i=n[1];U.useEffect(()=>{var t=t=>{t.key===`Escape`&&i({dismissed:!0,dismissedAtCoordinate:{x:e.coordinate?.x??0,y:e.coordinate?.y??0}})};return document.addEventListener(`keydown`,t),()=>{document.removeEventListener(`keydown`,t)}},[e.coordinate?.x,e.coordinate?.y]),r.dismissed&&((e.coordinate?.x??0)!==r.dismissedAtCoordinate.x||(e.coordinate?.y??0)!==r.dismissedAtCoordinate.y)&&i(J(J({},r),{},{dismissed:!1}));var a=Wt({allowEscapeViewBox:e.allowEscapeViewBox,coordinate:e.coordinate,offsetLeft:typeof e.offset==`number`?e.offset:e.offset.x,offsetTop:typeof e.offset==`number`?e.offset:e.offset.y,position:e.position,reverseDirection:e.reverseDirection,tooltipBox:e.lastBoundingBox,useTranslate3d:e.useTranslate3d,viewBox:e.viewBox}),o=a.cssClasses,s=a.cssProperties,c=J(J({},e.hasPortalFromProps?{}:J(J({transition:tn({prefersReducedMotion:t,isAnimationActive:e.isAnimationActive,active:e.active,animationDuration:e.animationDuration,animationEasing:e.animationEasing})},s),{},{pointerEvents:`none`,position:`absolute`,top:0,left:0})),{},{visibility:!r.dismissed&&e.active&&e.hasPayload?`visible`:`hidden`},e.wrapperStyle);return U.createElement(`div`,{xmlns:`http://www.w3.org/1999/xhtml`,tabIndex:-1,className:o,style:c,ref:e.innerRef},e.children)}var rn=U.memo(nn),an=[`x`,`y`,`top`,`left`,`width`,`height`,`className`];function on(){return on=Object.assign?Object.assign.bind():function(e){for(var t=1;t`M${e},${i}v${r}M${a},${t}h${n}`,hn=e=>{var t=e.x,n=t===void 0?0:t,r=e.y,i=r===void 0?0:r,a=e.top,o=a===void 0?0:a,s=e.left,c=s===void 0?0:s,l=e.width,u=l===void 0?0:l,d=e.height,f=d===void 0?0:d,p=e.className,m=fn(e,an),h=cn({x:n,y:i,top:o,left:c,width:u,height:f},m);return!z(n)||!z(i)||!z(u)||!z(f)||!z(o)||!z(c)?null:U.createElement(`path`,on({},pe(h),{className:A(`recharts-cross`,p),d:mn(n,i,u,f,o,c)}))};function gn(e,t,n,r){var i=r/2;return{stroke:`none`,fill:`#ccc`,x:e===`horizontal`?t.x-i:n.left+.5,y:e===`horizontal`?n.top+.5:t.y-i,width:e===`horizontal`?r:n.width-1,height:e===`horizontal`?n.height-1:r}}function _n(e){var t=e.cx,n=e.cy,r=e.radius,i=e.startAngle,a=e.endAngle;return{points:[R(t,n,r,i),R(t,n,r,a)],cx:t,cy:n,radius:r,startAngle:i,endAngle:a}}var vn,yn,bn,xn,Sn,Cn,wn;function Tn(){return Tn=Object.assign?Object.assign.bind():function(e){for(var t=1;tEe(t-e)*Math.min(Math.abs(t-e),359.999),X=e=>{var t=e.cx,n=e.cy,r=e.radius,i=e.angle,a=e.sign,o=e.isExternal,s=e.cornerRadius,c=e.cornerIsExternal,l=s*(o?1:-1)+r,u=Math.asin(s/l)/v,d=c?i:i+a*u,f=R(t,n,l,d),p=R(t,n,r,d),m=c?i-a*u:i;return{center:f,circleTangency:p,lineTangency:R(t,n,l*Math.cos(u*v),m),theta:u}},Dn=e=>{var t=e.cx,n=e.cy,r=e.innerRadius,i=e.outerRadius,a=e.startAngle,o=e.endAngle,s=En(a,o),c=a+s,l=R(t,n,i,a),u=R(t,n,i,c),d=F(vn||=Y([`M `,`,`,` diff --git a/internal/webui/static/assets/CostChart-CnmoPAdy.js b/internal/webui/static/assets/CostChart-CAPQoZh9.js similarity index 96% rename from internal/webui/static/assets/CostChart-CnmoPAdy.js rename to internal/webui/static/assets/CostChart-CAPQoZh9.js index 120f15bc..216783b4 100644 --- a/internal/webui/static/assets/CostChart-CnmoPAdy.js +++ b/internal/webui/static/assets/CostChart-CAPQoZh9.js @@ -1 +1 @@ -import{n as e,t}from"./CartesianGrid-CCDEyja0.js";import{Tn as n,_n as r,a as i,at as a,gn as o,i as s,r as c,t as l,vn as u,wn as d,xn as f}from"./index-0LMr5JvV.js";var p=n(d(),1),m=f(),h=[`var(--accent)`,`var(--muted)`,`var(--success)`,`var(--warning)`];function g(e,t,n){let r=new Date(e);return Number.isFinite(r.getTime())?new Intl.DateTimeFormat(n===`zh`?`zh-CN`:`en-US`,t?{month:`short`,day:`numeric`}:{month:`short`,day:`numeric`,hour:`2-digit`,minute:`2-digit`,hour12:!1}).format(r):``}function _(e,t){return t===`credits`?e.per_model_credits??{}:t===`tokens`?e.per_model_tokens??{}:e.per_model??{}}function v(e,t){return t===`cost`?o(e):t===`tokens`?r(e):u(e)}function y(e,t){return t===`cost`?`¥${e>=10?Math.round(e):Number(e.toFixed(1))}`:t===`tokens`?r(e):u(e)}function b({active:e,payload:t,label:n,daily:r,lang:i,models:a,totalLabel:o,metric:s}){if(!e||!t?.length)return null;let c=new Map(a.map(e=>[e.id,e.label])),l=t.filter(e=>(e.value??0)>0).sort((e,t)=>t.value-e.value),u=t.reduce((e,t)=>e+(t.value??0),0);return(0,m.jsxs)(`div`,{className:`rounded-xl bg-overlay px-3 py-2 text-caption text-overlay-foreground shadow-overlay`,children:[(0,m.jsx)(`div`,{className:`mono text-muted`,children:g(String(n),r,i)}),(0,m.jsxs)(`div`,{className:`mt-1.5 grid gap-1`,children:[l.map(e=>(0,m.jsxs)(`div`,{className:`flex items-center justify-between gap-6`,children:[(0,m.jsxs)(`span`,{className:`inline-flex items-center gap-1.5`,children:[(0,m.jsx)(`span`,{className:`size-1.5 rounded-full`,style:{background:h[a.findIndex(t=>t.id===e.dataKey)%h.length]}}),c.get(e.dataKey)||e.dataKey]}),(0,m.jsx)(`span`,{className:`mono font-medium`,children:v(e.value,s)})]},e.dataKey)),(0,m.jsxs)(`div`,{className:`flex items-center justify-between gap-6 border-t border-separator pt-1`,children:[(0,m.jsx)(`span`,{className:`text-muted`,children:o}),(0,m.jsx)(`span`,{className:`mono font-medium`,children:v(u,s)})]})]})]})}function x({series:n,models:r,lang:o,emptyLabel:u,totalLabel:d,metric:f=`credits`}){let v=(0,p.useMemo)(()=>r.map(e=>e.id),[r]),x=(0,p.useMemo)(()=>n.map(e=>{let t=_(e,f),n={at:e.at};for(let e of v){let r=Number(t[e]??0);n[e]=f===`tokens`?r:Number(r.toFixed(4))}return n}),[n,v,f]),S=Math.max(0,...x.map(e=>v.reduce((t,n)=>t+Number(e[n]??0),0)))>0,C=(x.length>1?Date.parse(x[x.length-1].at)-Date.parse(x[0].at):0)>1728e5;return!n.length||!S?(0,m.jsx)(`div`,{className:`grid min-h-56 place-items-center px-6 py-12 text-center text-sm text-muted`,children:u}):(0,m.jsxs)(`div`,{className:`min-w-0`,children:[(0,m.jsx)(`div`,{className:`h-72 w-full`,children:(0,m.jsx)(a,{width:`100%`,height:`100%`,children:(0,m.jsxs)(l,{data:x,margin:{top:8,right:8,bottom:0,left:0},children:[(0,m.jsx)(`defs`,{children:v.map((e,t)=>(0,m.jsxs)(`linearGradient`,{id:`cost-${f}-${e}`,x1:`0`,y1:`0`,x2:`0`,y2:`1`,children:[(0,m.jsx)(`stop`,{offset:`0%`,stopColor:h[t%h.length],stopOpacity:.32}),(0,m.jsx)(`stop`,{offset:`100%`,stopColor:h[t%h.length],stopOpacity:.04})]},e))}),(0,m.jsx)(t,{vertical:!1,stroke:`var(--separator)`,strokeDasharray:`3 6`}),(0,m.jsx)(s,{dataKey:`at`,tickLine:!1,axisLine:!1,minTickGap:28,tick:{fill:`var(--muted)`,fontSize:10,fontFamily:`var(--font-mono)`},tickFormatter:e=>g(e,C,o)}),(0,m.jsx)(c,{width:52,tickLine:!1,axisLine:!1,tick:{fill:`var(--muted)`,fontSize:10,fontFamily:`var(--font-mono)`},tickFormatter:e=>y(e,f)}),(0,m.jsx)(e,{cursor:{stroke:`var(--border)`,strokeDasharray:`3 3`},content:(0,m.jsx)(b,{daily:C,lang:o,models:r,totalLabel:d,metric:f})}),v.map((e,t)=>(0,m.jsx)(i,{type:`monotone`,dataKey:e,stackId:`usage`,stroke:h[t%h.length],strokeWidth:1.5,fill:`url(#cost-${f}-${e})`,isAnimationActive:!1,dot:!1,activeDot:{r:3,strokeWidth:1.2}},e))]})})}),(0,m.jsx)(`div`,{className:`mt-2 flex flex-wrap items-center gap-x-4 gap-y-1 text-[10px] text-muted`,children:r.map((e,t)=>(0,m.jsxs)(`span`,{className:`inline-flex items-center gap-1.5`,children:[(0,m.jsx)(`span`,{className:`size-1.5 rounded-full`,style:{background:h[t%h.length]}}),e.label]},e.id))})]})}export{x as CostChart}; \ No newline at end of file +import{n as e,t}from"./CartesianGrid-7x6C3-nK.js";import{Tn as n,_n as r,a as i,at as a,gn as o,i as s,r as c,t as l,vn as u,wn as d,xn as f}from"./index-DccK5bwD.js";var p=n(d(),1),m=f(),h=[`var(--accent)`,`var(--muted)`,`var(--success)`,`var(--warning)`];function g(e,t,n){let r=new Date(e);return Number.isFinite(r.getTime())?new Intl.DateTimeFormat(n===`zh`?`zh-CN`:`en-US`,t?{month:`short`,day:`numeric`}:{month:`short`,day:`numeric`,hour:`2-digit`,minute:`2-digit`,hour12:!1}).format(r):``}function _(e,t){return t===`credits`?e.per_model_credits??{}:t===`tokens`?e.per_model_tokens??{}:e.per_model??{}}function v(e,t){return t===`cost`?o(e):t===`tokens`?r(e):u(e)}function y(e,t){return t===`cost`?`¥${e>=10?Math.round(e):Number(e.toFixed(1))}`:t===`tokens`?r(e):u(e)}function b({active:e,payload:t,label:n,daily:r,lang:i,models:a,totalLabel:o,metric:s}){if(!e||!t?.length)return null;let c=new Map(a.map(e=>[e.id,e.label])),l=t.filter(e=>(e.value??0)>0).sort((e,t)=>t.value-e.value),u=t.reduce((e,t)=>e+(t.value??0),0);return(0,m.jsxs)(`div`,{className:`rounded-xl bg-overlay px-3 py-2 text-caption text-overlay-foreground shadow-overlay`,children:[(0,m.jsx)(`div`,{className:`mono text-muted`,children:g(String(n),r,i)}),(0,m.jsxs)(`div`,{className:`mt-1.5 grid gap-1`,children:[l.map(e=>(0,m.jsxs)(`div`,{className:`flex items-center justify-between gap-6`,children:[(0,m.jsxs)(`span`,{className:`inline-flex items-center gap-1.5`,children:[(0,m.jsx)(`span`,{className:`size-1.5 rounded-full`,style:{background:h[a.findIndex(t=>t.id===e.dataKey)%h.length]}}),c.get(e.dataKey)||e.dataKey]}),(0,m.jsx)(`span`,{className:`mono font-medium`,children:v(e.value,s)})]},e.dataKey)),(0,m.jsxs)(`div`,{className:`flex items-center justify-between gap-6 border-t border-separator pt-1`,children:[(0,m.jsx)(`span`,{className:`text-muted`,children:o}),(0,m.jsx)(`span`,{className:`mono font-medium`,children:v(u,s)})]})]})]})}function x({series:n,models:r,lang:o,emptyLabel:u,totalLabel:d,metric:f=`credits`}){let v=(0,p.useMemo)(()=>r.map(e=>e.id),[r]),x=(0,p.useMemo)(()=>n.map(e=>{let t=_(e,f),n={at:e.at};for(let e of v){let r=Number(t[e]??0);n[e]=f===`tokens`?r:Number(r.toFixed(4))}return n}),[n,v,f]),S=Math.max(0,...x.map(e=>v.reduce((t,n)=>t+Number(e[n]??0),0)))>0,C=(x.length>1?Date.parse(x[x.length-1].at)-Date.parse(x[0].at):0)>1728e5;return!n.length||!S?(0,m.jsx)(`div`,{className:`grid min-h-56 place-items-center px-6 py-12 text-center text-sm text-muted`,children:u}):(0,m.jsxs)(`div`,{className:`min-w-0`,children:[(0,m.jsx)(`div`,{className:`h-72 w-full`,children:(0,m.jsx)(a,{width:`100%`,height:`100%`,children:(0,m.jsxs)(l,{data:x,margin:{top:8,right:8,bottom:0,left:0},children:[(0,m.jsx)(`defs`,{children:v.map((e,t)=>(0,m.jsxs)(`linearGradient`,{id:`cost-${f}-${e}`,x1:`0`,y1:`0`,x2:`0`,y2:`1`,children:[(0,m.jsx)(`stop`,{offset:`0%`,stopColor:h[t%h.length],stopOpacity:.32}),(0,m.jsx)(`stop`,{offset:`100%`,stopColor:h[t%h.length],stopOpacity:.04})]},e))}),(0,m.jsx)(t,{vertical:!1,stroke:`var(--separator)`,strokeDasharray:`3 6`}),(0,m.jsx)(s,{dataKey:`at`,tickLine:!1,axisLine:!1,minTickGap:28,tick:{fill:`var(--muted)`,fontSize:10,fontFamily:`var(--font-mono)`},tickFormatter:e=>g(e,C,o)}),(0,m.jsx)(c,{width:52,tickLine:!1,axisLine:!1,tick:{fill:`var(--muted)`,fontSize:10,fontFamily:`var(--font-mono)`},tickFormatter:e=>y(e,f)}),(0,m.jsx)(e,{cursor:{stroke:`var(--border)`,strokeDasharray:`3 3`},content:(0,m.jsx)(b,{daily:C,lang:o,models:r,totalLabel:d,metric:f})}),v.map((e,t)=>(0,m.jsx)(i,{type:`monotone`,dataKey:e,stackId:`usage`,stroke:h[t%h.length],strokeWidth:1.5,fill:`url(#cost-${f}-${e})`,isAnimationActive:!1,dot:!1,activeDot:{r:3,strokeWidth:1.2}},e))]})})}),(0,m.jsx)(`div`,{className:`mt-2 flex flex-wrap items-center gap-x-4 gap-y-1 text-[10px] text-muted`,children:r.map((e,t)=>(0,m.jsxs)(`span`,{className:`inline-flex items-center gap-1.5`,children:[(0,m.jsx)(`span`,{className:`size-1.5 rounded-full`,style:{background:h[t%h.length]}}),e.label]},e.id))})]})}export{x as CostChart}; \ No newline at end of file diff --git a/internal/webui/static/assets/LiveSparkline-BQhFYYja.js b/internal/webui/static/assets/LiveSparkline-B-x3TPYA.js similarity index 80% rename from internal/webui/static/assets/LiveSparkline-BQhFYYja.js rename to internal/webui/static/assets/LiveSparkline-B-x3TPYA.js index 1e1f0d5b..0268bfe2 100644 --- a/internal/webui/static/assets/LiveSparkline-BQhFYYja.js +++ b/internal/webui/static/assets/LiveSparkline-B-x3TPYA.js @@ -1 +1 @@ -import{a as e,at as t,r as n,t as r,xn as i}from"./index-0LMr5JvV.js";var a=i();function o({samples:i,capacity:o,label:s}){let c=i.length?i:[{at:0,value:0}],l=Math.max(1,o,...c.map(e=>e.value));return(0,a.jsx)(`div`,{className:`h-6 w-[120px] shrink-0`,role:`img`,"aria-label":s,children:(0,a.jsx)(t,{width:`100%`,height:`100%`,children:(0,a.jsxs)(r,{data:c,margin:{top:1,right:0,bottom:1,left:0},children:[(0,a.jsx)(n,{hide:!0,domain:[0,l]}),(0,a.jsx)(e,{type:`monotone`,dataKey:`value`,stroke:`var(--foreground)`,strokeOpacity:.7,strokeWidth:1.25,fill:`var(--foreground)`,fillOpacity:.08,isAnimationActive:!1,dot:!1})]})})})}export{o as LiveSparkline}; \ No newline at end of file +import{a as e,at as t,r as n,t as r,xn as i}from"./index-DccK5bwD.js";var a=i();function o({samples:i,capacity:o,label:s}){let c=i.length?i:[{at:0,value:0}],l=Math.max(1,o,...c.map(e=>e.value));return(0,a.jsx)(`div`,{className:`h-6 w-[120px] shrink-0`,role:`img`,"aria-label":s,children:(0,a.jsx)(t,{width:`100%`,height:`100%`,children:(0,a.jsxs)(r,{data:c,margin:{top:1,right:0,bottom:1,left:0},children:[(0,a.jsx)(n,{hide:!0,domain:[0,l]}),(0,a.jsx)(e,{type:`monotone`,dataKey:`value`,stroke:`var(--foreground)`,strokeOpacity:.7,strokeWidth:1.25,fill:`var(--foreground)`,fillOpacity:.08,isAnimationActive:!1,dot:!1})]})})})}export{o as LiveSparkline}; \ No newline at end of file diff --git a/internal/webui/static/assets/TrafficChart-3sVQJ2N-.js b/internal/webui/static/assets/TrafficChart-CqsCWiLZ.js similarity index 99% rename from internal/webui/static/assets/TrafficChart-3sVQJ2N-.js rename to internal/webui/static/assets/TrafficChart-CqsCWiLZ.js index f278f035..4aed69c2 100644 --- a/internal/webui/static/assets/TrafficChart-3sVQJ2N-.js +++ b/internal/webui/static/assets/TrafficChart-CqsCWiLZ.js @@ -1 +1 @@ -import{n as e,t}from"./CartesianGrid-CCDEyja0.js";import{$ as n,$t as r,B as i,C as a,D as o,G as s,Q as c,Qt as l,S as u,Sn as d,T as f,Tn as p,U as m,V as h,W as g,Y as _,Yt as v,_ as y,_n as b,_t as x,a as S,at as C,b as w,bn as T,c as E,ct as D,d as ee,dn as O,f as k,fn as A,ft as j,g as M,h as te,hn as ne,i as re,in as N,k as P,lt as F,m as I,mn as ie,mt as L,n as ae,o as oe,p as se,pt as R,r as ce,rn as le,s as ue,st as z,tt as de,un as fe,v as pe,w as me,wn as he,x as ge,xn as _e,y as ve,yn as B,yt as ye}from"./index-0LMr5JvV.js";var V=p(he()),be=[`animationElapsedTime`,`isAnimating`,`isEntrance`,`visibleLength`,`strokeDasharray`,`connectNulls`];function H(){return H=Object.assign?Object.assign.bind():function(e){for(var t=1;te+t,0);if(!i)return U(t,e);for(var a=Math.floor(e/i),o=e%i,s=[],c=0,l=0;co){s=[...r.slice(0,c),o-l];break}}var f=s.length%2==0?[0,t]:[t];return[...Te(r,a),...s,...f].map(e=>`${e}px`).join(`, `)}function De(e,t,n){return e?Ee(n,t,`${e}`.split(/[,\s]+/gim).map(e=>parseFloat(e))):U(t,n)}function Oe(e){e.animationElapsedTime,e.isAnimating,e.isEntrance;var t=e.visibleLength,n=e.strokeDasharray,r=e.connectNulls,i=xe(e,be),a=r??!1,o;if(t!=null){var s=i.pathRef;o=De(n,Ce(s?.current??null),t)}else n!=null&&(o=String(n));return V.createElement(_,H({},i,{connectNulls:a,strokeDasharray:o}))}function ke(e){var t=(0,V.useRef)(0),n=(0,V.useRef)(0),r=(0,V.useRef)(!1),i=(0,V.useRef)(e);return i.current!==e&&(t.current=n.current,i.current=e),(0,V.useCallback)((e,i)=>{if(r.current)return null;var a=Math.min(N(t.current+e*i),i);return e>0&&i>0&&(n.current=Math.max(n.current,a),a>=i)?(r.current=!0,null):a},[])}var Ae=[`children`];function je(e,t){if(e==null)return{};var n,r,i=Me(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r({x:0,y:0,value:0}),errorBarOffset:0});function Pe(e){var t=e.children,n=je(e,Ae);return V.createElement(Ne.Provider,{value:n},t)}var W=(e,t,n,r)=>i(e,`xAxis`,t,r),G=(e,t,n,r)=>h(e,`xAxis`,t,r),K=(e,t,n,r)=>i(e,`yAxis`,n,r),q=(e,t,n,r)=>h(e,`yAxis`,n,r),Fe=x([n,W,K,G,q],(e,t,n,r,i)=>L(e,`xAxis`)?D(t,r,!1):D(n,i,!1)),Ie=(e,t,n,r,i)=>i;function Le(e){return e.type===`line`}var Re=x([m,Ie],(e,t)=>e.filter(Le).find(e=>e.id===t)),ze=x([n,W,K,G,q,Re,Fe,s],(e,t,n,r,i,a,o,s)=>{var c=s.chartData,l=s.dataStartIndex,u=s.dataEndIndex;if(!(a==null||t==null||n==null||r==null||i==null||r.length===0||i.length===0||o==null||e!==`horizontal`&&e!==`vertical`)){var d=a.dataKey,f=a.data,p=f!=null&&f.length>0?f:c?.slice(l,u+1);if(p!=null)return at({layout:e,xAxis:t,yAxis:n,xAxisTicks:r,yAxisTicks:i,dataKey:d,bandSize:o,displayedData:p})}}),Be=[`id`],Ve=[`type`,`layout`,`connectNulls`,`needClip`,`shape`,`strokeDasharray`],He=[`activeDot`,`animateNewValues`,`animationBegin`,`animationDuration`,`animationEasing`,`connectNulls`,`dot`,`hide`,`isAnimationActive`,`label`,`legendType`,`xAxisId`,`yAxisId`,`id`];function J(){return J=Object.assign?Object.assign.bind():function(e){for(var t=1;t0?t/n:0}var Q={activeDot:!0,animateNewValues:!0,animationBegin:0,animationDuration:1500,animationEasing:`ease`,animationInterpolateFn:(e,t)=>{if(e==null)return[];if(t===1)return e.flatMap(e=>e.status===`removed`?[]:[e.next]);var n=Je(e),r=[];for(var i of e)if(i.status===`matched`)r.push(Z(Z({},i.next),{},{x:l(i.prev.x,i.next.x,t),y:l(i.prev.y,i.next.y,t)}));else if(i.status===`added`){if(i.next.x!=null){var a=i.next.x-n;r.push(Z(Z({},i.next),{},{x:l(a,i.next.x,t),y:i.next.y}))}else r.push(i.next)}else if(i.status===`removed`&&i.prev.x!=null){var o=i.prev.x+n;r.push(Z(Z({},i.prev),{},{x:l(i.prev.x,o,t),y:i.prev.y}))}return r},animationMatchBy:ve,connectNulls:!1,dot:!0,fill:`#fff`,hide:!1,isAnimationActive:`auto`,label:!1,legendType:`line`,shape:Oe,stroke:`#3182bd`,strokeWidth:1,xAxisId:0,yAxisId:0,zIndex:g.line,type:`linear`},Ye=e=>{var t=e.dataKey,n=e.name,r=e.stroke,i=e.legendType;return[{inactive:e.hide,dataKey:t,type:i,color:r,value:j(n,t),payload:e}]},Xe=V.memo(e=>{var t=e.dataKey,n=e.data,r=e.stroke,i=e.strokeWidth,a=e.fill,o=e.name,s=e.hide,c=e.unit,l=e.formatter,u=e.tooltipType,d=e.id,f={dataDefinedOnItem:n,getPosition:le,settings:{stroke:r,strokeWidth:i,fill:a,dataKey:t,nameKey:void 0,name:j(o,t),hide:s,type:u,color:r,unit:c,formatter:l,graphicalItemId:d}};return V.createElement(ge,{tooltipEntrySettings:f})});function Ze(e){var t=e.clipPathId,n=e.points,r=e.props,i=r.dot,a=r.dataKey,o=r.needClip;r.id;var s=Y(r,Be),c=A(s);return V.createElement(I,{points:n,dot:i,className:`recharts-line-dots`,dotClassName:`recharts-line-dot`,dataKey:a,baseProps:c,needClip:o,clipPathId:t})}function Qe(e){var t=e.showLabels,n=e.children,r=e.points,i=(0,V.useMemo)(()=>r?.map(e=>{var t={x:e.x??0,y:e.y??0,width:0,lowerWidth:0,upperWidth:0,height:0};return Z(Z({},t),{},{value:e.value,payload:e.payload,viewBox:t,parentViewBox:void 0,fill:void 0})}),[r]);return V.createElement(me,{value:t?i:void 0},n)}function $e(e){var t=e.clipPathId,n=e.pathRef,r=e.points,i=e.props,a=e.animationElapsedTime,o=e.isAnimating,s=e.isEntrance,c=e.visibleLength,l=i.type,d=i.layout,f=i.connectNulls,p=i.needClip,m=i.shape,h=i.strokeDasharray,g=Y(i,Ve),_=Z(Z({},O(g)),{},{fill:`none`,className:`recharts-line-curve`,clipPath:p?`url(#clipPath-${t})`:void 0,points:r,type:l,layout:d,connectNulls:f,strokeDasharray:h??i.strokeDasharray,pathRef:n,animationElapsedTime:a,isAnimating:o,isEntrance:i.animateNewValues?s:!1,visibleLength:c});return V.createElement(V.Fragment,null,r?.length>1&&V.createElement(u,{option:m,DefaultShape:Q.shape,shapeProps:_}),V.createElement(Ze,{points:r,clipPathId:t,props:i}))}function et(e){var t=e.clipPathId,n=e.props,r=e.pathRef,i=e.previousPointsRef,a=n.points,o=n.isAnimationActive,s=n.animationBegin,c=n.animationDuration,l=n.animationEasing,u=n.animationMatchBy,d=n.animationInterpolateFn,p=n.layout,m=qe(r.current),h=pe(n.onAnimationStart,n.onAnimationEnd),g=h.isAnimating,_=h.handleAnimationStart,v=h.handleAnimationEnd,b=!g,x=ke(a),S=(0,V.useCallback)(e=>e>0&&m>0,[m]);return V.createElement(Qe,{points:a,showLabels:b},n.children,V.createElement(y,{animationInput:a,animationIdPrefix:`recharts-line-`,items:a,previousItemsRef:i,isAnimationActive:o,animationBegin:s,animationDuration:c,animationEasing:l,onAnimationStart:_,onAnimationEnd:v,animationInterpolateFn:d,animationMatchBy:u,shouldUpdatePreviousRef:S,layout:p},(e,i,a)=>{var o=g||i<1,s=o?x(i,m):null;return V.createElement($e,{props:n,points:e,clipPathId:t,pathRef:r,animationElapsedTime:i,isAnimating:o,isEntrance:a,visibleLength:s})}),V.createElement(f,{label:n.label}))}function tt(e){var t=e.clipPathId,n=e.props,r=(0,V.useRef)(null),i=(0,V.useRef)(null);return V.createElement(et,{props:n,clipPathId:t,previousPointsRef:r,pathRef:i})}var nt=(e,t)=>({x:e.x??void 0,y:e.y??void 0,value:e.value,errorVal:R(e.payload,t)}),rt=class extends V.Component{render(){var e=this.props,t=e.hide,n=e.dot,r=e.points,i=e.className,o=e.xAxisId,s=e.yAxisId,c=e.top,l=e.left,u=e.width,f=e.height,p=e.id,m=e.needClip,h=e.zIndex;if(t)return null;var g=d(`recharts-line`,i),_=p,v=oe(n),y=v.r,b=v.strokeWidth,x=a(n),S=y*2+b,C=m?`url(#clipPath-${x?``:`dots-`}${_})`:void 0;return V.createElement(P,{zIndex:h},V.createElement(fe,{className:g},m&&V.createElement(`defs`,null,V.createElement(ue,{clipPathId:_,xAxisId:o,yAxisId:s}),!x&&V.createElement(`clipPath`,{id:`clipPath-dots-${_}`},V.createElement(`rect`,{x:l-S/2,y:c-S/2,width:u+S,height:f+S}))),V.createElement(Pe,{xAxisId:o,yAxisId:s,data:r,dataPointFormatter:nt,errorBarOffset:0},V.createElement(tt,{props:this.props,clipPathId:_}))),V.createElement(k,{activeDot:this.props.activeDot,points:r,mainColor:this.props.stroke,itemDataKey:this.props.dataKey,clipPath:C}))}};function it(e){var t=v(e,Q),n=t.activeDot,r=t.animateNewValues,i=t.animationBegin,a=t.animationDuration,o=t.animationEasing,s=t.connectNulls,c=t.dot,l=t.hide,u=t.isAnimationActive,d=t.label,f=t.legendType,p=t.xAxisId,m=t.yAxisId,h=t.id,g=Y(t,He),_=E(p,m).needClip,y=se(),b=de(),x=z(),S=ye(e=>ze(e,p,m,x,h));if(b!==`horizontal`&&b!==`vertical`||S==null||y==null)return null;var C=y.height,w=y.width,T=y.x,D=y.y;return V.createElement(rt,J({},g,{id:h,connectNulls:s,dot:c,activeDot:n,animateNewValues:r,animationBegin:i,animationDuration:a,animationEasing:o,isAnimationActive:u,hide:l,label:d,legendType:f,xAxisId:p,yAxisId:m,points:S,layout:b,height:C,width:w,left:T,top:D,needClip:_}))}function at(e){var t=e.layout,n=e.xAxis,i=e.yAxis,a=e.xAxisTicks,o=e.yAxisTicks,s=e.dataKey,c=e.bandSize;return e.displayedData.map((e,l)=>{var u=R(e,s);if(t===`horizontal`)return{x:F({axis:n,ticks:a,bandSize:c,entry:e,index:l}),y:(r(u)?null:i.scale.map(u))??null,value:u,payload:e};var d=r(u)?null:n.scale.map(u),f=F({axis:i,ticks:o,bandSize:c,entry:e,index:l});return d==null||f==null?null:{x:d,y:f,value:u,payload:e}}).filter(Boolean)}function ot(e){var t=v(e,Q),n=z();return V.createElement(M,{id:t.id,type:`line`},e=>V.createElement(V.Fragment,null,V.createElement(w,{legendPayload:Ye(t)}),V.createElement(Xe,{dataKey:t.dataKey,data:t.data,stroke:t.stroke,strokeWidth:t.strokeWidth,fill:t.fill,name:t.name,hide:t.hide,unit:t.unit,formatter:t.formatter,tooltipType:t.tooltipType,id:e}),V.createElement(te,{type:`line`,id:e,data:t.data,xAxisId:t.xAxisId,yAxisId:t.yAxisId,zAxisId:0,dataKey:t.dataKey,hide:t.hide,isPanorama:n}),V.createElement(it,J({},t,{id:e}))))}var st=V.memo(ot,c);st.displayName=`Line`;var ct=[`axis`],lt=(0,V.forwardRef)((e,t)=>V.createElement(ae,{chartName:`ComposedChart`,defaultTooltipEventType:`axis`,validateTooltipEventTypes:ct,tooltipPayloadSearcher:o,categoricalChartProps:e,ref:t})),ut={metric:24,title:18,section:15,body:13,xs:12,caption:12},$=_e();function dt(e,t,n){let r=new Date(e);return Number.isFinite(r.getTime())?new Intl.DateTimeFormat(n===`zh`?`zh-CN`:`en-US`,t?{month:`short`,day:`numeric`}:{hour:`2-digit`,minute:`2-digit`,hour12:!1}).format(r):``}function ft(e){if(e<=1)return 1;let t=10**Math.floor(Math.log10(e)),n=e/t;return(n<=1?1:n<=2?2:n<=5?5:10)*t}function pt(e){let t=Math.max(0,e.ok??e.requests-e.error),n=Math.min(Math.max(0,e.requests-t),Math.max(0,e.requests));return{...e,okCount:t,errorCount:n,inputTokens:e.prompt_tokens??0,outputTokens:e.completion_tokens??0,cacheTokens:e.cache_read_tokens??0,p50:e.latency_p50_ms??null,p95:e.latency_p95_ms??null}}function mt({active:e,payload:t,daily:n,lang:r,specs:i,format:a,totalLabel:o,total:s}){if(!e||!t?.[0])return null;let c=t[0].payload;return(0,$.jsxs)(`div`,{className:`rounded-xl bg-overlay px-3 py-2 text-caption text-overlay-foreground shadow-overlay`,children:[(0,$.jsx)(`div`,{className:`mono text-muted`,children:dt(c.at,n,r)}),(0,$.jsxs)(`div`,{className:`mt-1.5 grid gap-1`,children:[i.map(e=>(0,$.jsxs)(`div`,{className:`flex items-center justify-between gap-6`,children:[(0,$.jsxs)(`span`,{className:`inline-flex items-center gap-1.5`,children:[(0,$.jsx)(`span`,{className:`size-1.5 rounded-full`,style:{background:e.color}}),e.label]}),(0,$.jsx)(`span`,{className:`mono font-medium`,children:a(c[e.key])})]},String(e.key))),s&&o?(0,$.jsxs)(`div`,{className:`flex items-center justify-between gap-6 pt-1`,children:[(0,$.jsx)(`span`,{className:`text-muted`,children:o}),(0,$.jsx)(`span`,{className:`mono font-medium`,children:a(s(c))})]}):null]})]})}function ht({series:n,lang:r,emptyLabel:i,okLabel:a,errorLabel:o,onSelectRange:s,selectHint:c,labels:l,initialMetric:u=`requests`}){let[d,f]=(0,V.useState)(u),[p,m]=(0,V.useState)(null),[h,g]=(0,V.useState)(null),_=(0,V.useRef)(!1),v=(0,V.useRef)(null),y=(0,V.useMemo)(()=>n.map(pt),[n]),x=y[0],w=y[y.length-1],E=(x&&w?Date.parse(w.at)-Date.parse(x.at):0)>1728e5,D=l??{requests:`Requests`,tokens:`Tokens`,latency:`p95 latency`,input:`input`,output:`output`,cacheRead:`cache read`,p50:`p50`,p95:`p95`,total:`total`},O=d===`tokens`?[{key:`inputTokens`,label:D.input,color:`var(--foreground)`,area:!0,width:1.5,opacity:.75},{key:`outputTokens`,label:D.output,color:`var(--success)`,area:!0,width:1.5},{key:`cacheTokens`,label:D.cacheRead,color:`var(--muted)`,width:1.25,opacity:.8}]:d===`latency`?[{key:`p95`,label:D.p95,color:`var(--warning)`,area:!0,width:1.75},{key:`p50`,label:D.p50,color:`var(--muted)`,width:1.25,opacity:.85}]:[{key:`okCount`,label:a,color:`var(--success)`,area:!0,width:1.75},{key:`errorCount`,label:o,color:`var(--danger)`,area:!0,width:1.25,opacity:.7}],k=e=>e==null?`—`:d===`latency`?B(e):b(e),A=d===`requests`?e=>e.requests:d===`tokens`?e=>e.inputTokens+e.outputTokens:void 0,j=y.flatMap(e=>O.map(t=>Number(e[t.key]??0))),M=d===`requests`?Math.max(0,...y.map(e=>e.requests)):Math.max(0,...j),te=ft(M),N=y.some(e=>e.requests>0),P=y.map(e=>`${e.at}:${e.requests}:${e.error}`).join(`|`);(0,V.useLayoutEffect)(()=>{let e=v.current;if(!e||!N||_.current)return;let t=!1,n=null,r=T.context(()=>{let r=T.matchMedia(),i=()=>{let t=T.utils.toArray(`.recharts-area-area`,e),n=T.utils.toArray(`.recharts-line-curve`,e);return!t.length&&!n.length?!1:(r.add(`(prefers-reduced-motion: reduce)`,()=>{T.set(n,{clearProps:`strokeDashoffset,strokeDasharray`}),T.set(t,{clearProps:`autoAlpha`})}),r.add(`(prefers-reduced-motion: no-preference)`,()=>{T.fromTo(t,{autoAlpha:0},{autoAlpha:1,duration:.45,ease:`power2.out`,stagger:.05}),n.forEach((e,t)=>{let n=e.getTotalLength();T.fromTo(e,{strokeDasharray:n,strokeDashoffset:n},{strokeDashoffset:0,duration:.7,delay:t*.05,ease:`power2.out`})})}),!0)};if(i()){t=!0,_.current=!0;return}n=new MutationObserver(()=>{t||i()&&(t=!0,_.current=!0,n?.disconnect())}),n.observe(e,{childList:!0,subtree:!0})},e);return()=>{n?.disconnect(),r.revert()}},[N,P]);let F=y.length>1?Date.parse(y[1].at)-Date.parse(y[0].at):36e5,I=()=>{if(p&&h&&s){let e=Date.parse(p),t=Date.parse(h);if(Number.isFinite(e)&&Number.isFinite(t)){let n=Math.min(e,t),r=Math.max(e,t)+F;s(new Date(n).toISOString(),new Date(r).toISOString())}}m(null),g(null)},L=e=>{let t=e?.activeLabel;return t==null?null:String(t)};return!n.length||!N?(0,$.jsx)(ne,{emptyLabel:i}):(0,$.jsxs)(`div`,{ref:v,className:`min-w-0`,children:[(0,$.jsx)(`div`,{className:`mb-3 flex justify-end`,children:(0,$.jsx)(ie,{value:d,onChange:e=>f(e),ariaLabel:D.requests,options:[{id:`requests`,label:D.requests},{id:`tokens`,label:D.tokens},{id:`latency`,label:D.latency}]})}),(0,$.jsx)(`div`,{className:`h-56 w-full`,children:(0,$.jsx)(C,{width:`100%`,height:`100%`,children:(0,$.jsxs)(lt,{data:y,margin:{top:8,right:8,bottom:0,left:0},style:s?{cursor:`crosshair`,userSelect:`none`}:void 0,onMouseDown:s?e=>{let t=L(e);t&&(m(t),g(t))}:void 0,onMouseMove:s?e=>{if(!p)return;let t=L(e);t&&g(t)}:void 0,onMouseUp:s?I:void 0,onMouseLeave:s?()=>{m(null),g(null)}:void 0,children:[(0,$.jsx)(`defs`,{children:O.filter(e=>e.area).map(e=>(0,$.jsxs)(`linearGradient`,{id:`traffic-fill-${String(e.key)}`,x1:`0`,y1:`0`,x2:`0`,y2:`1`,children:[(0,$.jsx)(`stop`,{offset:`0%`,stopColor:e.color,stopOpacity:.22}),(0,$.jsx)(`stop`,{offset:`100%`,stopColor:e.color,stopOpacity:.02})]},String(e.key)))}),(0,$.jsx)(t,{vertical:!1,stroke:`var(--separator)`,strokeDasharray:`3 6`}),(0,$.jsx)(re,{dataKey:`at`,tickLine:!1,axisLine:!1,minTickGap:28,tick:{fill:`var(--muted)`,fontSize:ut.caption,fontFamily:`var(--font-mono)`},tickFormatter:e=>dt(e,E,r)}),(0,$.jsx)(ce,{width:d===`latency`?44:36,domain:[0,te],tickLine:!1,axisLine:!1,allowDecimals:!1,tick:{fill:`var(--muted)`,fontSize:ut.caption,fontFamily:`var(--font-mono)`},tickFormatter:e=>d===`latency`?B(e):b(e)}),(0,$.jsx)(e,{cursor:{stroke:`var(--border)`,strokeDasharray:`3 3`},content:(0,$.jsx)(mt,{daily:E,lang:r,specs:O,format:k,totalLabel:D.total,total:A})}),O.filter(e=>e.area).map(e=>(0,$.jsx)(S,{type:`monotone`,dataKey:e.key,stroke:`none`,fill:`url(#traffic-fill-${String(e.key)})`,isAnimationActive:!1,activeDot:!1,dot:!1,connectNulls:!0},`area-${String(e.key)}`)),O.map(e=>(0,$.jsx)(st,{type:`monotone`,dataKey:e.key,stroke:e.color,strokeWidth:e.width??1.5,strokeOpacity:e.opacity??1,dot:!1,isAnimationActive:!1,connectNulls:!0,activeDot:{r:4,fill:`var(--surface)`,stroke:e.color,strokeWidth:1.5}},`line-${String(e.key)}`)),p&&h?(0,$.jsx)(ee,{x1:p,x2:h,fill:`var(--foreground)`,fillOpacity:.06,stroke:`none`,ifOverflow:`visible`}):null]})})}),(0,$.jsxs)(`div`,{className:`mt-2 flex min-h-5 items-center justify-between gap-3 text-caption text-muted`,children:[(0,$.jsx)(`div`,{className:`flex items-center gap-3`,children:O.map(e=>(0,$.jsxs)(`span`,{className:`inline-flex items-center gap-1.5`,children:[(0,$.jsx)(`span`,{className:`size-1.5 rounded-full`,style:{background:e.color}}),e.label]},String(e.key)))}),(0,$.jsxs)(`span`,{className:`flex items-center gap-3`,children:[s&&c?(0,$.jsx)(`span`,{children:c}):null,(0,$.jsxs)(`span`,{className:`mono`,children:[k(M),` peak`]})]})]})]})}export{ht as TrafficChart}; \ No newline at end of file +import{n as e,t}from"./CartesianGrid-7x6C3-nK.js";import{$ as n,$t as r,B as i,C as a,D as o,G as s,Q as c,Qt as l,S as u,Sn as d,T as f,Tn as p,U as m,V as h,W as g,Y as _,Yt as v,_ as y,_n as b,_t as x,a as S,at as C,b as w,bn as T,c as E,ct as D,d as ee,dn as O,f as k,fn as A,ft as j,g as M,h as te,hn as ne,i as re,in as N,k as P,lt as F,m as I,mn as ie,mt as L,n as ae,o as oe,p as se,pt as R,r as ce,rn as le,s as ue,st as z,tt as de,un as fe,v as pe,w as me,wn as he,x as ge,xn as _e,y as ve,yn as B,yt as ye}from"./index-DccK5bwD.js";var V=p(he()),be=[`animationElapsedTime`,`isAnimating`,`isEntrance`,`visibleLength`,`strokeDasharray`,`connectNulls`];function H(){return H=Object.assign?Object.assign.bind():function(e){for(var t=1;te+t,0);if(!i)return U(t,e);for(var a=Math.floor(e/i),o=e%i,s=[],c=0,l=0;co){s=[...r.slice(0,c),o-l];break}}var f=s.length%2==0?[0,t]:[t];return[...Te(r,a),...s,...f].map(e=>`${e}px`).join(`, `)}function De(e,t,n){return e?Ee(n,t,`${e}`.split(/[,\s]+/gim).map(e=>parseFloat(e))):U(t,n)}function Oe(e){e.animationElapsedTime,e.isAnimating,e.isEntrance;var t=e.visibleLength,n=e.strokeDasharray,r=e.connectNulls,i=xe(e,be),a=r??!1,o;if(t!=null){var s=i.pathRef;o=De(n,Ce(s?.current??null),t)}else n!=null&&(o=String(n));return V.createElement(_,H({},i,{connectNulls:a,strokeDasharray:o}))}function ke(e){var t=(0,V.useRef)(0),n=(0,V.useRef)(0),r=(0,V.useRef)(!1),i=(0,V.useRef)(e);return i.current!==e&&(t.current=n.current,i.current=e),(0,V.useCallback)((e,i)=>{if(r.current)return null;var a=Math.min(N(t.current+e*i),i);return e>0&&i>0&&(n.current=Math.max(n.current,a),a>=i)?(r.current=!0,null):a},[])}var Ae=[`children`];function je(e,t){if(e==null)return{};var n,r,i=Me(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r({x:0,y:0,value:0}),errorBarOffset:0});function Pe(e){var t=e.children,n=je(e,Ae);return V.createElement(Ne.Provider,{value:n},t)}var W=(e,t,n,r)=>i(e,`xAxis`,t,r),G=(e,t,n,r)=>h(e,`xAxis`,t,r),K=(e,t,n,r)=>i(e,`yAxis`,n,r),q=(e,t,n,r)=>h(e,`yAxis`,n,r),Fe=x([n,W,K,G,q],(e,t,n,r,i)=>L(e,`xAxis`)?D(t,r,!1):D(n,i,!1)),Ie=(e,t,n,r,i)=>i;function Le(e){return e.type===`line`}var Re=x([m,Ie],(e,t)=>e.filter(Le).find(e=>e.id===t)),ze=x([n,W,K,G,q,Re,Fe,s],(e,t,n,r,i,a,o,s)=>{var c=s.chartData,l=s.dataStartIndex,u=s.dataEndIndex;if(!(a==null||t==null||n==null||r==null||i==null||r.length===0||i.length===0||o==null||e!==`horizontal`&&e!==`vertical`)){var d=a.dataKey,f=a.data,p=f!=null&&f.length>0?f:c?.slice(l,u+1);if(p!=null)return at({layout:e,xAxis:t,yAxis:n,xAxisTicks:r,yAxisTicks:i,dataKey:d,bandSize:o,displayedData:p})}}),Be=[`id`],Ve=[`type`,`layout`,`connectNulls`,`needClip`,`shape`,`strokeDasharray`],He=[`activeDot`,`animateNewValues`,`animationBegin`,`animationDuration`,`animationEasing`,`connectNulls`,`dot`,`hide`,`isAnimationActive`,`label`,`legendType`,`xAxisId`,`yAxisId`,`id`];function J(){return J=Object.assign?Object.assign.bind():function(e){for(var t=1;t0?t/n:0}var Q={activeDot:!0,animateNewValues:!0,animationBegin:0,animationDuration:1500,animationEasing:`ease`,animationInterpolateFn:(e,t)=>{if(e==null)return[];if(t===1)return e.flatMap(e=>e.status===`removed`?[]:[e.next]);var n=Je(e),r=[];for(var i of e)if(i.status===`matched`)r.push(Z(Z({},i.next),{},{x:l(i.prev.x,i.next.x,t),y:l(i.prev.y,i.next.y,t)}));else if(i.status===`added`){if(i.next.x!=null){var a=i.next.x-n;r.push(Z(Z({},i.next),{},{x:l(a,i.next.x,t),y:i.next.y}))}else r.push(i.next)}else if(i.status===`removed`&&i.prev.x!=null){var o=i.prev.x+n;r.push(Z(Z({},i.prev),{},{x:l(i.prev.x,o,t),y:i.prev.y}))}return r},animationMatchBy:ve,connectNulls:!1,dot:!0,fill:`#fff`,hide:!1,isAnimationActive:`auto`,label:!1,legendType:`line`,shape:Oe,stroke:`#3182bd`,strokeWidth:1,xAxisId:0,yAxisId:0,zIndex:g.line,type:`linear`},Ye=e=>{var t=e.dataKey,n=e.name,r=e.stroke,i=e.legendType;return[{inactive:e.hide,dataKey:t,type:i,color:r,value:j(n,t),payload:e}]},Xe=V.memo(e=>{var t=e.dataKey,n=e.data,r=e.stroke,i=e.strokeWidth,a=e.fill,o=e.name,s=e.hide,c=e.unit,l=e.formatter,u=e.tooltipType,d=e.id,f={dataDefinedOnItem:n,getPosition:le,settings:{stroke:r,strokeWidth:i,fill:a,dataKey:t,nameKey:void 0,name:j(o,t),hide:s,type:u,color:r,unit:c,formatter:l,graphicalItemId:d}};return V.createElement(ge,{tooltipEntrySettings:f})});function Ze(e){var t=e.clipPathId,n=e.points,r=e.props,i=r.dot,a=r.dataKey,o=r.needClip;r.id;var s=Y(r,Be),c=A(s);return V.createElement(I,{points:n,dot:i,className:`recharts-line-dots`,dotClassName:`recharts-line-dot`,dataKey:a,baseProps:c,needClip:o,clipPathId:t})}function Qe(e){var t=e.showLabels,n=e.children,r=e.points,i=(0,V.useMemo)(()=>r?.map(e=>{var t={x:e.x??0,y:e.y??0,width:0,lowerWidth:0,upperWidth:0,height:0};return Z(Z({},t),{},{value:e.value,payload:e.payload,viewBox:t,parentViewBox:void 0,fill:void 0})}),[r]);return V.createElement(me,{value:t?i:void 0},n)}function $e(e){var t=e.clipPathId,n=e.pathRef,r=e.points,i=e.props,a=e.animationElapsedTime,o=e.isAnimating,s=e.isEntrance,c=e.visibleLength,l=i.type,d=i.layout,f=i.connectNulls,p=i.needClip,m=i.shape,h=i.strokeDasharray,g=Y(i,Ve),_=Z(Z({},O(g)),{},{fill:`none`,className:`recharts-line-curve`,clipPath:p?`url(#clipPath-${t})`:void 0,points:r,type:l,layout:d,connectNulls:f,strokeDasharray:h??i.strokeDasharray,pathRef:n,animationElapsedTime:a,isAnimating:o,isEntrance:i.animateNewValues?s:!1,visibleLength:c});return V.createElement(V.Fragment,null,r?.length>1&&V.createElement(u,{option:m,DefaultShape:Q.shape,shapeProps:_}),V.createElement(Ze,{points:r,clipPathId:t,props:i}))}function et(e){var t=e.clipPathId,n=e.props,r=e.pathRef,i=e.previousPointsRef,a=n.points,o=n.isAnimationActive,s=n.animationBegin,c=n.animationDuration,l=n.animationEasing,u=n.animationMatchBy,d=n.animationInterpolateFn,p=n.layout,m=qe(r.current),h=pe(n.onAnimationStart,n.onAnimationEnd),g=h.isAnimating,_=h.handleAnimationStart,v=h.handleAnimationEnd,b=!g,x=ke(a),S=(0,V.useCallback)(e=>e>0&&m>0,[m]);return V.createElement(Qe,{points:a,showLabels:b},n.children,V.createElement(y,{animationInput:a,animationIdPrefix:`recharts-line-`,items:a,previousItemsRef:i,isAnimationActive:o,animationBegin:s,animationDuration:c,animationEasing:l,onAnimationStart:_,onAnimationEnd:v,animationInterpolateFn:d,animationMatchBy:u,shouldUpdatePreviousRef:S,layout:p},(e,i,a)=>{var o=g||i<1,s=o?x(i,m):null;return V.createElement($e,{props:n,points:e,clipPathId:t,pathRef:r,animationElapsedTime:i,isAnimating:o,isEntrance:a,visibleLength:s})}),V.createElement(f,{label:n.label}))}function tt(e){var t=e.clipPathId,n=e.props,r=(0,V.useRef)(null),i=(0,V.useRef)(null);return V.createElement(et,{props:n,clipPathId:t,previousPointsRef:r,pathRef:i})}var nt=(e,t)=>({x:e.x??void 0,y:e.y??void 0,value:e.value,errorVal:R(e.payload,t)}),rt=class extends V.Component{render(){var e=this.props,t=e.hide,n=e.dot,r=e.points,i=e.className,o=e.xAxisId,s=e.yAxisId,c=e.top,l=e.left,u=e.width,f=e.height,p=e.id,m=e.needClip,h=e.zIndex;if(t)return null;var g=d(`recharts-line`,i),_=p,v=oe(n),y=v.r,b=v.strokeWidth,x=a(n),S=y*2+b,C=m?`url(#clipPath-${x?``:`dots-`}${_})`:void 0;return V.createElement(P,{zIndex:h},V.createElement(fe,{className:g},m&&V.createElement(`defs`,null,V.createElement(ue,{clipPathId:_,xAxisId:o,yAxisId:s}),!x&&V.createElement(`clipPath`,{id:`clipPath-dots-${_}`},V.createElement(`rect`,{x:l-S/2,y:c-S/2,width:u+S,height:f+S}))),V.createElement(Pe,{xAxisId:o,yAxisId:s,data:r,dataPointFormatter:nt,errorBarOffset:0},V.createElement(tt,{props:this.props,clipPathId:_}))),V.createElement(k,{activeDot:this.props.activeDot,points:r,mainColor:this.props.stroke,itemDataKey:this.props.dataKey,clipPath:C}))}};function it(e){var t=v(e,Q),n=t.activeDot,r=t.animateNewValues,i=t.animationBegin,a=t.animationDuration,o=t.animationEasing,s=t.connectNulls,c=t.dot,l=t.hide,u=t.isAnimationActive,d=t.label,f=t.legendType,p=t.xAxisId,m=t.yAxisId,h=t.id,g=Y(t,He),_=E(p,m).needClip,y=se(),b=de(),x=z(),S=ye(e=>ze(e,p,m,x,h));if(b!==`horizontal`&&b!==`vertical`||S==null||y==null)return null;var C=y.height,w=y.width,T=y.x,D=y.y;return V.createElement(rt,J({},g,{id:h,connectNulls:s,dot:c,activeDot:n,animateNewValues:r,animationBegin:i,animationDuration:a,animationEasing:o,isAnimationActive:u,hide:l,label:d,legendType:f,xAxisId:p,yAxisId:m,points:S,layout:b,height:C,width:w,left:T,top:D,needClip:_}))}function at(e){var t=e.layout,n=e.xAxis,i=e.yAxis,a=e.xAxisTicks,o=e.yAxisTicks,s=e.dataKey,c=e.bandSize;return e.displayedData.map((e,l)=>{var u=R(e,s);if(t===`horizontal`)return{x:F({axis:n,ticks:a,bandSize:c,entry:e,index:l}),y:(r(u)?null:i.scale.map(u))??null,value:u,payload:e};var d=r(u)?null:n.scale.map(u),f=F({axis:i,ticks:o,bandSize:c,entry:e,index:l});return d==null||f==null?null:{x:d,y:f,value:u,payload:e}}).filter(Boolean)}function ot(e){var t=v(e,Q),n=z();return V.createElement(M,{id:t.id,type:`line`},e=>V.createElement(V.Fragment,null,V.createElement(w,{legendPayload:Ye(t)}),V.createElement(Xe,{dataKey:t.dataKey,data:t.data,stroke:t.stroke,strokeWidth:t.strokeWidth,fill:t.fill,name:t.name,hide:t.hide,unit:t.unit,formatter:t.formatter,tooltipType:t.tooltipType,id:e}),V.createElement(te,{type:`line`,id:e,data:t.data,xAxisId:t.xAxisId,yAxisId:t.yAxisId,zAxisId:0,dataKey:t.dataKey,hide:t.hide,isPanorama:n}),V.createElement(it,J({},t,{id:e}))))}var st=V.memo(ot,c);st.displayName=`Line`;var ct=[`axis`],lt=(0,V.forwardRef)((e,t)=>V.createElement(ae,{chartName:`ComposedChart`,defaultTooltipEventType:`axis`,validateTooltipEventTypes:ct,tooltipPayloadSearcher:o,categoricalChartProps:e,ref:t})),ut={metric:24,title:18,section:15,body:13,xs:12,caption:12},$=_e();function dt(e,t,n){let r=new Date(e);return Number.isFinite(r.getTime())?new Intl.DateTimeFormat(n===`zh`?`zh-CN`:`en-US`,t?{month:`short`,day:`numeric`}:{hour:`2-digit`,minute:`2-digit`,hour12:!1}).format(r):``}function ft(e){if(e<=1)return 1;let t=10**Math.floor(Math.log10(e)),n=e/t;return(n<=1?1:n<=2?2:n<=5?5:10)*t}function pt(e){let t=Math.max(0,e.ok??e.requests-e.error),n=Math.min(Math.max(0,e.requests-t),Math.max(0,e.requests));return{...e,okCount:t,errorCount:n,inputTokens:e.prompt_tokens??0,outputTokens:e.completion_tokens??0,cacheTokens:e.cache_read_tokens??0,p50:e.latency_p50_ms??null,p95:e.latency_p95_ms??null}}function mt({active:e,payload:t,daily:n,lang:r,specs:i,format:a,totalLabel:o,total:s}){if(!e||!t?.[0])return null;let c=t[0].payload;return(0,$.jsxs)(`div`,{className:`rounded-xl bg-overlay px-3 py-2 text-caption text-overlay-foreground shadow-overlay`,children:[(0,$.jsx)(`div`,{className:`mono text-muted`,children:dt(c.at,n,r)}),(0,$.jsxs)(`div`,{className:`mt-1.5 grid gap-1`,children:[i.map(e=>(0,$.jsxs)(`div`,{className:`flex items-center justify-between gap-6`,children:[(0,$.jsxs)(`span`,{className:`inline-flex items-center gap-1.5`,children:[(0,$.jsx)(`span`,{className:`size-1.5 rounded-full`,style:{background:e.color}}),e.label]}),(0,$.jsx)(`span`,{className:`mono font-medium`,children:a(c[e.key])})]},String(e.key))),s&&o?(0,$.jsxs)(`div`,{className:`flex items-center justify-between gap-6 pt-1`,children:[(0,$.jsx)(`span`,{className:`text-muted`,children:o}),(0,$.jsx)(`span`,{className:`mono font-medium`,children:a(s(c))})]}):null]})]})}function ht({series:n,lang:r,emptyLabel:i,okLabel:a,errorLabel:o,onSelectRange:s,selectHint:c,labels:l,initialMetric:u=`requests`}){let[d,f]=(0,V.useState)(u),[p,m]=(0,V.useState)(null),[h,g]=(0,V.useState)(null),_=(0,V.useRef)(!1),v=(0,V.useRef)(null),y=(0,V.useMemo)(()=>n.map(pt),[n]),x=y[0],w=y[y.length-1],E=(x&&w?Date.parse(w.at)-Date.parse(x.at):0)>1728e5,D=l??{requests:`Requests`,tokens:`Tokens`,latency:`p95 latency`,input:`input`,output:`output`,cacheRead:`cache read`,p50:`p50`,p95:`p95`,total:`total`},O=d===`tokens`?[{key:`inputTokens`,label:D.input,color:`var(--foreground)`,area:!0,width:1.5,opacity:.75},{key:`outputTokens`,label:D.output,color:`var(--success)`,area:!0,width:1.5},{key:`cacheTokens`,label:D.cacheRead,color:`var(--muted)`,width:1.25,opacity:.8}]:d===`latency`?[{key:`p95`,label:D.p95,color:`var(--warning)`,area:!0,width:1.75},{key:`p50`,label:D.p50,color:`var(--muted)`,width:1.25,opacity:.85}]:[{key:`okCount`,label:a,color:`var(--success)`,area:!0,width:1.75},{key:`errorCount`,label:o,color:`var(--danger)`,area:!0,width:1.25,opacity:.7}],k=e=>e==null?`—`:d===`latency`?B(e):b(e),A=d===`requests`?e=>e.requests:d===`tokens`?e=>e.inputTokens+e.outputTokens:void 0,j=y.flatMap(e=>O.map(t=>Number(e[t.key]??0))),M=d===`requests`?Math.max(0,...y.map(e=>e.requests)):Math.max(0,...j),te=ft(M),N=y.some(e=>e.requests>0),P=y.map(e=>`${e.at}:${e.requests}:${e.error}`).join(`|`);(0,V.useLayoutEffect)(()=>{let e=v.current;if(!e||!N||_.current)return;let t=!1,n=null,r=T.context(()=>{let r=T.matchMedia(),i=()=>{let t=T.utils.toArray(`.recharts-area-area`,e),n=T.utils.toArray(`.recharts-line-curve`,e);return!t.length&&!n.length?!1:(r.add(`(prefers-reduced-motion: reduce)`,()=>{T.set(n,{clearProps:`strokeDashoffset,strokeDasharray`}),T.set(t,{clearProps:`autoAlpha`})}),r.add(`(prefers-reduced-motion: no-preference)`,()=>{T.fromTo(t,{autoAlpha:0},{autoAlpha:1,duration:.45,ease:`power2.out`,stagger:.05}),n.forEach((e,t)=>{let n=e.getTotalLength();T.fromTo(e,{strokeDasharray:n,strokeDashoffset:n},{strokeDashoffset:0,duration:.7,delay:t*.05,ease:`power2.out`})})}),!0)};if(i()){t=!0,_.current=!0;return}n=new MutationObserver(()=>{t||i()&&(t=!0,_.current=!0,n?.disconnect())}),n.observe(e,{childList:!0,subtree:!0})},e);return()=>{n?.disconnect(),r.revert()}},[N,P]);let F=y.length>1?Date.parse(y[1].at)-Date.parse(y[0].at):36e5,I=()=>{if(p&&h&&s){let e=Date.parse(p),t=Date.parse(h);if(Number.isFinite(e)&&Number.isFinite(t)){let n=Math.min(e,t),r=Math.max(e,t)+F;s(new Date(n).toISOString(),new Date(r).toISOString())}}m(null),g(null)},L=e=>{let t=e?.activeLabel;return t==null?null:String(t)};return!n.length||!N?(0,$.jsx)(ne,{emptyLabel:i}):(0,$.jsxs)(`div`,{ref:v,className:`min-w-0`,children:[(0,$.jsx)(`div`,{className:`mb-3 flex justify-end`,children:(0,$.jsx)(ie,{value:d,onChange:e=>f(e),ariaLabel:D.requests,options:[{id:`requests`,label:D.requests},{id:`tokens`,label:D.tokens},{id:`latency`,label:D.latency}]})}),(0,$.jsx)(`div`,{className:`h-56 w-full`,children:(0,$.jsx)(C,{width:`100%`,height:`100%`,children:(0,$.jsxs)(lt,{data:y,margin:{top:8,right:8,bottom:0,left:0},style:s?{cursor:`crosshair`,userSelect:`none`}:void 0,onMouseDown:s?e=>{let t=L(e);t&&(m(t),g(t))}:void 0,onMouseMove:s?e=>{if(!p)return;let t=L(e);t&&g(t)}:void 0,onMouseUp:s?I:void 0,onMouseLeave:s?()=>{m(null),g(null)}:void 0,children:[(0,$.jsx)(`defs`,{children:O.filter(e=>e.area).map(e=>(0,$.jsxs)(`linearGradient`,{id:`traffic-fill-${String(e.key)}`,x1:`0`,y1:`0`,x2:`0`,y2:`1`,children:[(0,$.jsx)(`stop`,{offset:`0%`,stopColor:e.color,stopOpacity:.22}),(0,$.jsx)(`stop`,{offset:`100%`,stopColor:e.color,stopOpacity:.02})]},String(e.key)))}),(0,$.jsx)(t,{vertical:!1,stroke:`var(--separator)`,strokeDasharray:`3 6`}),(0,$.jsx)(re,{dataKey:`at`,tickLine:!1,axisLine:!1,minTickGap:28,tick:{fill:`var(--muted)`,fontSize:ut.caption,fontFamily:`var(--font-mono)`},tickFormatter:e=>dt(e,E,r)}),(0,$.jsx)(ce,{width:d===`latency`?44:36,domain:[0,te],tickLine:!1,axisLine:!1,allowDecimals:!1,tick:{fill:`var(--muted)`,fontSize:ut.caption,fontFamily:`var(--font-mono)`},tickFormatter:e=>d===`latency`?B(e):b(e)}),(0,$.jsx)(e,{cursor:{stroke:`var(--border)`,strokeDasharray:`3 3`},content:(0,$.jsx)(mt,{daily:E,lang:r,specs:O,format:k,totalLabel:D.total,total:A})}),O.filter(e=>e.area).map(e=>(0,$.jsx)(S,{type:`monotone`,dataKey:e.key,stroke:`none`,fill:`url(#traffic-fill-${String(e.key)})`,isAnimationActive:!1,activeDot:!1,dot:!1,connectNulls:!0},`area-${String(e.key)}`)),O.map(e=>(0,$.jsx)(st,{type:`monotone`,dataKey:e.key,stroke:e.color,strokeWidth:e.width??1.5,strokeOpacity:e.opacity??1,dot:!1,isAnimationActive:!1,connectNulls:!0,activeDot:{r:4,fill:`var(--surface)`,stroke:e.color,strokeWidth:1.5}},`line-${String(e.key)}`)),p&&h?(0,$.jsx)(ee,{x1:p,x2:h,fill:`var(--foreground)`,fillOpacity:.06,stroke:`none`,ifOverflow:`visible`}):null]})})}),(0,$.jsxs)(`div`,{className:`mt-2 flex min-h-5 items-center justify-between gap-3 text-caption text-muted`,children:[(0,$.jsx)(`div`,{className:`flex items-center gap-3`,children:O.map(e=>(0,$.jsxs)(`span`,{className:`inline-flex items-center gap-1.5`,children:[(0,$.jsx)(`span`,{className:`size-1.5 rounded-full`,style:{background:e.color}}),e.label]},String(e.key)))}),(0,$.jsxs)(`span`,{className:`flex items-center gap-3`,children:[s&&c?(0,$.jsx)(`span`,{children:c}):null,(0,$.jsxs)(`span`,{className:`mono`,children:[k(M),` peak`]})]})]})]})}export{ht as TrafficChart}; \ No newline at end of file diff --git a/internal/webui/static/assets/index-0LMr5JvV.js b/internal/webui/static/assets/index-DccK5bwD.js similarity index 73% rename from internal/webui/static/assets/index-0LMr5JvV.js rename to internal/webui/static/assets/index-DccK5bwD.js index 2304ce80..92fe585e 100644 --- a/internal/webui/static/assets/index-0LMr5JvV.js +++ b/internal/webui/static/assets/index-DccK5bwD.js @@ -1,4 +1,4 @@ -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/TrafficChart-3sVQJ2N-.js","assets/CartesianGrid-CCDEyja0.js","assets/CostChart-CnmoPAdy.js"])))=>i.map(i=>d[i]); +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/TrafficChart-CqsCWiLZ.js","assets/CartesianGrid-7x6C3-nK.js","assets/CostChart-CAPQoZh9.js"])))=>i.map(i=>d[i]); var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,n)=>{let r={};for(var i in e)t(r,i,{get:e[i],enumerable:!0});return n||t(r,Symbol.toStringTag,{value:`Module`}),r},c=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},l=(n,r,o)=>(o=n==null?{}:e(i(n)),c(r||!n||!n.__esModule||!a.call(n,`default`)?t(o,`default`,{value:n,enumerable:!0}):o,n));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var u=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.iterator;function m(e){return typeof e!=`object`||!e?null:(e=p&&e[p]||e[`@@iterator`],typeof e==`function`?e:null)}var h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},g=Object.assign,_={};function v(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}v.prototype.isReactComponent={},v.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},v.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function y(){}y.prototype=v.prototype;function b(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}var x=b.prototype=new y;x.constructor=b,g(x,v.prototype),x.isPureReactComponent=!0;var S=Array.isArray;function C(){}var w={H:null,A:null,T:null,S:null},T=Object.prototype.hasOwnProperty;function E(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function D(e,t){return E(e.type,t,e.props)}function O(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function k(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var A=/\/+/g;function j(e,t){return typeof e==`object`&&e&&e.key!=null?k(``+e.key):t.toString(36)}function M(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(C,C):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function N(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,N(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+j(e,0):a,S(o)?(i=``,c!=null&&(i=c.replace(A,`$&/`)+`/`),N(o,r,i,``,function(e){return e})):o!=null&&(O(o)&&(o=D(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(A,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(S(e))for(var u=0;u{t.exports=u()})),f=o((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(e.unstable_now=void 0,typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=!1,_=typeof setTimeout==`function`?setTimeout:null,v=typeof clearTimeout==`function`?clearTimeout:null,y=typeof setImmediate<`u`?setImmediate:null;function b(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function x(e){if(h=!1,b(e),!m){if(n(c)!==null)m=!0,S||(S=!0,O());else{var t=n(l);t!==null&&j(x,t.startTime-e)}}}var S=!1,C=-1,w=5,T=-1;function E(){return g?!0:!(e.unstable_now()-Tt&&E());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=t);if(t=e.unstable_now(),typeof s==`function`){d.callback=s,b(t),i=!0;break b}d===n(c)&&r(c),b(t)}else r(c);d=n(c)}if(d!==null)i=!0;else{var u=n(l);u!==null&&j(x,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}i=void 0}}finally{i?O():S=!1}}}var O;if(typeof y==`function`)O=function(){y(D)};else if(typeof MessageChannel<`u`){var k=new MessageChannel,A=k.port2;k.port1.onmessage=D,O=function(){A.postMessage(null)}}else O=function(){_(D,0)};function j(t,n){C=_(function(){t(e.unstable_now())},n)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(e){e.callback=null},e.unstable_forceFrameRate=function(e){0>e||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(v(C),C=-1):h=!0,j(x,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,S||(S=!0,O()))),r},e.unstable_shouldYield=E,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),p=o(((e,t)=>{t.exports=f()})),m=o((e=>{var t=d();function n(e){var t=`https://react.dev/errors/`+e;if(1{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=m()})),g=o((e=>{var t=p(),n=d(),r=h();function i(e){var t=`https://react.dev/errors/`+e;if(1B||(e.current=z[B],z[B]=null,B--)}function ee(e,t){B++,z[B]=e.current,e.current=t}var te=V(null),ne=V(null),re=V(null),ie=V(null);function ae(e,t){switch(ee(re,t),ee(ne,e),ee(te,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Yd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Yd(t),e=Xd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}H(te),ee(te,e)}function oe(){H(te),H(ne),H(re)}function se(e){e.memoizedState!==null&&ee(ie,e);var t=te.current,n=Xd(t,e.type);t!==n&&(ee(ne,e),ee(te,n))}function ce(e){ne.current===e&&(H(te),H(ne)),ie.current===e&&(H(ie),op._currentValue=R)}var le,ue;function de(e){if(le===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);le=t&&t[1]||``,ue=-1)`:-1>>=0,e===0?32:31-(Pe(e)/Fe|0)|0}var Le=256,Re=262144,ze=4194304;function Be(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function Ve(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=Be(n))):i=Be(o):i=Be(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=Be(n))):i=Be(o)):i=Be(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function He(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Ue(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function We(){var e=ze;return ze<<=1,!(ze&62914560)&&(ze=4194304),e}function Ge(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function Ke(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function qe(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),on=!1;if(an)try{var sn={};Object.defineProperty(sn,"passive",{get:function(){on=!0}}),window.addEventListener(`test`,sn,sn),window.removeEventListener(`test`,sn,sn)}catch{on=!1}var cn=null,ln=null,un=null;function dn(){if(un)return un;var e,t=ln,n=t.length,r,i=`value`in cn?cn.value:cn.textContent,a=i.length;for(e=0;e=Un),Kn=` `,qn=!1;function Jn(e,t){switch(e){case`keyup`:return Vn.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Yn(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var Xn=!1;function Zn(e,t){switch(e){case`compositionend`:return Yn(t);case`keypress`:return t.which===32?(qn=!0,Kn):null;case`textInput`:return e=t.data,e===Kn&&qn?null:e;default:return null}}function Qn(e,t){if(Xn)return e===`compositionend`||!Hn&&Jn(e,t)?(e=dn(),un=ln=cn=null,Xn=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=br(n)}}function Sr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Sr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Cr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Mt(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=Mt(e.document)}return t}function wr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var Tr=an&&`documentMode`in document&&11>=document.documentMode,Er=null,Dr=null,Or=null,kr=!1;function Ar(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;kr||Er==null||Er!==Mt(r)||(r=Er,`selectionStart`in r&&wr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Or&&yr(Or,r)||(Or=r,r=Nd(Dr,`onSelect`),0>=o,i-=o,Si=1<<32-Ne(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),ji&&wi(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),ji&&wi(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return ji&&wi(a,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),ji&&wi(a,g),u}function b(e,r,o,c){if(typeof o==`object`&&o&&o.type===y&&o.key===null&&(o=o.props.children),typeof o==`object`&&o){switch(o.$$typeof){case _:a:{for(var l=o.key;r!==null;){if(r.key===l){if(l=o.type,l===y){if(r.tag===7){n(e,r.sibling),c=a(r,o.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===O&&Ta(l)===r.type){n(e,r.sibling),c=a(r,o.props),Ma(c,o),c.return=e,e=c;break a}n(e,r);break}t(e,r),r=r.sibling}o.type===y?(c=li(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=ci(o.type,o.key,o.props,null,e.mode,c),Ma(c,o),c.return=e,e=c)}return s(e);case v:a:{for(l=o.key;r!==null;){if(r.key===l){if(r.tag===4&&r.stateNode.containerInfo===o.containerInfo&&r.stateNode.implementation===o.implementation){n(e,r.sibling),c=a(r,o.children||[]),c.return=e,e=c;break a}n(e,r);break}t(e,r),r=r.sibling}c=fi(o,e.mode,c),c.return=e,e=c}return s(e);case O:return o=Ta(o),b(e,r,o,c)}if(F(o))return h(e,r,o,c);if(M(o)){if(l=M(o),typeof l!=`function`)throw Error(i(150));return o=l.call(o),g(e,r,o,c)}if(typeof o.then==`function`)return b(e,r,ja(o),c);if(o.$$typeof===C)return b(e,r,$i(e,o),c);Na(e,o)}return typeof o==`string`&&o!==``||typeof o==`number`||typeof o==`bigint`?(o=``+o,r!==null&&r.tag===6?(n(e,r.sibling),c=a(r,o),c.return=e,e=c):(n(e,r),c=ui(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{Aa=0;var i=b(e,t,n,r);return ka=null,i}catch(t){if(t===ya||t===xa)throw t;var a=ii(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var Fa=Pa(!0),Ia=Pa(!1),La=!1;function Ra(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function za(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Ba(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Va(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,zl&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=ti(e),ei(e,null,n),t}return Zr(e,r,t,n),ti(e)}function Ha(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Ye(e,n)}}function Ua(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var Wa=!1;function Ga(){if(Wa){var e=ua;if(e!==null)throw e}}function Ka(e,t,n,r){Wa=!1;var i=e.updateQueue;La=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,p=f!==s.lane;if(p?(Hl&f)===f:(r&f)===f){f!==0&&f===la&&(Wa=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var h=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(h=g.payload,typeof h==`function`){d=h.call(_,d,f);break a}d=h;break a;case 3:h.flags=h.flags&-65537|128;case 0:if(h=g.payload,f=typeof h==`function`?h.call(_,d,f):h,f==null)break a;d=m({},d,f);break a;case 2:La=!0}}f=s.callback,f!==null&&(e.flags|=64,p&&(e.flags|=8192),p=i.callbacks,p===null?i.callbacks=[f]:p.push(f))}else p={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;p=s,s=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),Xl|=o,e.lanes=o,e.memoizedState=d}}function qa(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function Ja(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=I.T,s={};I.T=s,Ns(e,!1,t,n);try{var c=i(),l=I.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Ms(e,t,pa(c,r),vu(e)):Ms(e,t,r,vu(e))}catch(n){Ms(e,t,{then:function(){},status:`rejected`,reason:n},vu())}finally{L.p=a,o!==null&&s.types!==null&&(o.types=s.types),I.T=o}}function Ss(){}function Cs(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=ws(e).queue;W(e,a,t,R,n===null?Ss:function(){return Ts(e),n(r)})}function ws(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:R,baseState:R,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Io,lastRenderedState:R},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Io,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function Ts(e){var t=ws(e);t.next===null&&(t=e.alternate.memoizedState),Ms(e,t.next.queue,{},vu())}function Es(){return Qi(op)}function Ds(){return jo().memoizedState}function Os(){return jo().memoizedState}function ks(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=vu();e=Ba(n);var r=Va(t,e,n);r!==null&&(bu(r,t,n),Ha(r,t,n)),t={cache:aa()},e.payload=t;return}t=t.return}}function As(e,t,n){var r=vu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Ps(e)?Fs(t,n):(n=Qr(e,t,n,r),n!==null&&(bu(n,e,r),Is(n,t,r)))}function js(e,t,n){Ms(e,t,n,vu())}function Ms(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Ps(e))Fs(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,vr(s,o))return Zr(e,t,i,0),Bl===null&&Xr(),!1}catch{}if(n=Qr(e,t,i,r),n!==null)return bu(n,e,r),Is(n,t,r),!0}return!1}function Ns(e,t,n,r){if(r={lane:2,revertLane:_d(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Ps(e)){if(t)throw Error(i(479))}else t=Qr(e,n,r,2),t!==null&&bu(t,e,2)}function Ps(e){var t=e.alternate;return e===uo||t!==null&&t===uo}function Fs(e,t){ho=mo=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Is(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Ye(e,n)}}var Ls={readContext:Qi,use:Po,useCallback:xo,useContext:xo,useEffect:xo,useImperativeHandle:xo,useLayoutEffect:xo,useInsertionEffect:xo,useMemo:xo,useReducer:xo,useRef:xo,useState:xo,useDebugValue:xo,useDeferredValue:xo,useTransition:xo,useSyncExternalStore:xo,useId:xo,useHostTransitionStatus:xo,useFormState:xo,useActionState:xo,useOptimistic:xo,useMemoCache:xo,useCacheRefresh:xo};Ls.useEffectEvent=xo;var Rs={readContext:Qi,use:Po,useCallback:function(e,t){return Ao().memoizedState=[e,t===void 0?null:t],e},useContext:Qi,useEffect:us,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),cs(4194308,4,hs.bind(null,t,e),n)},useLayoutEffect:function(e,t){return cs(4194308,4,e,t)},useInsertionEffect:function(e,t){cs(4,2,e,t)},useMemo:function(e,t){var n=Ao();t=t===void 0?null:t;var r=e();if(go){Me(!0);try{e()}finally{Me(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=Ao();if(n!==void 0){var i=n(t);if(go){Me(!0);try{n(t)}finally{Me(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=As.bind(null,uo,e),[r.memoizedState,e]},useRef:function(e){var t=Ao();return e={current:e},t.memoizedState=e},useState:function(e){e=Ko(e);var t=e.queue,n=js.bind(null,uo,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:_s,useDeferredValue:function(e,t){return bs(Ao(),e,t)},useTransition:function(){var e=Ko(!1);return e=W.bind(null,uo,e.queue,!0,!1),Ao().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=uo,a=Ao();if(ji){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),Bl===null)throw Error(i(349));Hl&127||Vo(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,us(Uo.bind(null,r,o,e),[e]),r.flags|=2048,os(9,{destroy:void 0},Ho.bind(null,r,o,n,t),null),n},useId:function(){var e=Ao(),t=Bl.identifierPrefix;if(ji){var n=Ci,r=Si;n=(r&~(1<<32-Ne(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=_o++,0<\/script>`,o=o.removeChild(o.firstChild);break;case`select`:o=typeof r.is==`string`?s.createElement(`select`,{is:r.is}):s.createElement(`select`),r.multiple?o.multiple=!0:r.size&&(o.size=r.size);break;default:o=typeof r.is==`string`?s.createElement(a,{is:r.is}):s.createElement(a)}}o[nt]=t,o[rt]=r;a:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)o.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break a;for(;s.sibling===null;){if(s.return===null||s.return===t)break a;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=o;a:switch(Hd(o,a,r),a){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Mc(t)}}return Ic(t),G(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Mc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=re.current,Ri(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=ki,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[nt]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||zd(e.nodeValue,n)),e||Fi(t,!0)}else e=Jd(e).createTextNode(r),e[nt]=t,t.stateNode=e}return Ic(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Ri(t),n!==null){if(e===null){if(!r)throw Error(i(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(557));e[nt]=t}else zi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Ic(t),e=!1}else n=Bi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(oo(t),t):(oo(t),null);if(t.flags&128)throw Error(i(558))}return Ic(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=Ri(t),r!==null&&r.dehydrated!==null){if(e===null){if(!a)throw Error(i(318));if(a=t.memoizedState,a=a===null?null:a.dehydrated,!a)throw Error(i(317));a[nt]=t}else zi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;Ic(t),a=!1}else a=Bi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(oo(t),t):(oo(t),null)}return oo(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,a=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(a=r.alternate.memoizedState.cachePool.pool),o=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),o!==a&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Pc(t,t.updateQueue),Ic(t),null);case 4:return oe(),e===null&&kd(t.stateNode.containerInfo),Ic(t),null;case 10:return Ki(t.type),Ic(t),null;case 19:if(H(so),r=t.memoizedState,r===null)return Ic(t),null;if(a=!!(t.flags&128),o=r.rendering,o===null){if(a)Fc(r,!1);else{if(Yl!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=co(e),o!==null){for(t.flags|=128,Fc(r,!1),e=o.updateQueue,t.updateQueue=e,Pc(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)si(n,e),n=n.sibling;return ee(so,so.current&1|2),ji&&wi(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&xe()>ou&&(t.flags|=128,a=!0,Fc(r,!1),t.lanes=4194304)}}else{if(!a){if(e=co(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Pc(t,e),Fc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!ji)return Ic(t),null}else 2*xe()-r.renderingStartTime>ou&&n!==536870912&&(t.flags|=128,a=!0,Fc(r,!1),t.lanes=4194304)}r.isBackwards?(o.sibling=t.child,t.child=o):(e=r.last,e===null?t.child=o:e.sibling=o,r.last=o)}return r.tail===null?(Ic(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=xe(),e.sibling=null,n=so.current,ee(so,a?n&1|2:n&1),ji&&wi(t,r.treeForkCount),e);case 22:case 23:return oo(t),$a(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(Ic(t),t.subtreeFlags&6&&(t.flags|=8192)):Ic(t),n=t.updateQueue,n!==null&&Pc(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&H(ha),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Ki(ia),Ic(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function Rc(e,t){switch(Di(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Ki(ia),oe(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return ce(t),null;case 31:if(t.memoizedState!==null){if(oo(t),t.alternate===null)throw Error(i(340));zi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(oo(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));zi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return H(so),null;case 4:return oe(),null;case 10:return Ki(t.type),null;case 22:case 23:return oo(t),$a(),e!==null&&H(ha),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Ki(ia),null;case 25:return null;default:return null}}function zc(e,t){switch(Di(t),t.tag){case 3:Ki(ia),oe();break;case 26:case 27:case 5:ce(t);break;case 4:oe();break;case 31:t.memoizedState!==null&&oo(t);break;case 13:oo(t);break;case 19:H(so);break;case 10:Ki(t.type);break;case 22:case 23:oo(t),$a(),e!==null&&H(ha);break;case 24:Ki(ia)}}function Bc(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Xu(t,t.return,e)}}function Vc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Xu(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Xu(t,t.return,e)}}function Hc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{Ja(t,n)}catch(t){Xu(e,e.return,t)}}}function Uc(e,t,n){n.props=Gs(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Xu(e,t,n)}}function Wc(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Xu(e,t,n)}}function Gc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null){if(typeof r==`function`)try{r()}catch(n){Xu(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Xu(e,t,n)}else n.current=null}}function Kc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Xu(e,e.return,t)}}function qc(e,t,n){try{var r=e.stateNode;Ud(r,e.type,n,t),r[rt]=t}catch(t){Xu(e,e.return,t)}}function Jc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&of(e.type)||e.tag===4}function Yc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Jc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&of(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Xc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Yt));else if(r!==4&&(r===27&&of(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Xc(e,t,n),e=e.sibling;e!==null;)Xc(e,t,n),e=e.sibling}function Zc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&of(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(Zc(e,t,n),e=e.sibling;e!==null;)Zc(e,t,n),e=e.sibling}function Qc(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Hd(t,r,n),t[nt]=e,t[rt]=n}catch(t){Xu(e,e.return,t)}}var $c=!1,el=!1,tl=!1,nl=typeof WeakSet==`function`?WeakSet:Set,rl=null;function il(e,t){if(e=e.containerInfo,Kd=hp,e=Cr(e),wr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var a=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||r!==0&&f.nodeType!==3||(l=s+r),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===r&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(qd={focusedElem:e,selectionRange:n},hp=!1,rl=t;rl!==null;)if(t=rl,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,rl=e;else for(;rl!==null;){switch(t=rl,o=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Hd(o,r,n),o[nt]=e,ht(o),r=o;break a;case`link`:var s=Yf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=xr(s,h),v=xr(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,I.T=null,n=mu,mu=null;var o=uu,s=fu;if(lu=0,du=uu=null,fu=0,zl&6)throw Error(i(331));var c=zl;if(zl|=4,Pl(o.current),El(o,o.current,s,n),zl=c,ud(0,!1),je&&typeof je.onPostCommitFiberRoot==`function`)try{je.onPostCommitFiberRoot(Ae,o)}catch{}return!0}finally{L.p=a,I.T=r,Ku(e,t)}}function Yu(e,t,n){t=mi(n,t),t=Zs(e.stateNode,t,2),e=Va(e,t,2),e!==null&&(Ke(e,2),ld(e))}function Xu(e,t,n){if(e.tag===3)Yu(e,e,n);else for(;t!==null;){if(t.tag===3){Yu(t,e,n);break}if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(cu===null||!cu.has(r))){e=mi(n,e),n=Qs(2),r=Va(t,n,2),r!==null&&($s(n,r,t,e),Ke(r,2),ld(r));break}}t=t.return}}function Zu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Rl;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(ql=!0,i.add(n),e=Qu.bind(null,e,t,n),t.then(e,e))}function Qu(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,Bl===e&&(Hl&n)===n&&(Yl===4||Yl===3&&(Hl&62914560)===Hl&&300>xe()-iu?!(zl&2)&&Du(e,0):Ql|=n,eu===Hl&&(eu=0)),ld(e)}function $u(e,t){t===0&&(t=We()),e=$r(e,t),e!==null&&(Ke(e,t),ld(e))}function ed(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),$u(e,n)}function td(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}r!==null&&r.delete(t),$u(e,n)}function nd(e,t){return _e(e,t)}var rd=null,id=null,ad=!1,od=!1,sd=!1,cd=0;function ld(e){e!==id&&e.next===null&&(id===null?rd=id=e:id=id.next=e),od=!0,ad||(ad=!0,gd())}function ud(e,t){if(!sd&&od){sd=!0;do for(var n=!1,r=rd;r!==null;){if(!t){if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-Ne(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,hd(r,a))}else a=Hl,a=Ve(r,r===Bl?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||He(r,a)||(n=!0,hd(r,a))}r=r.next}while(n);sd=!1}}function dd(){fd()}function fd(){od=ad=!1;var e=0;cd!==0&&$d()&&(e=cd);for(var t=xe(),n=null,r=rd;r!==null;){var i=r.next,a=pd(r,t);a===0?(r.next=null,n===null?rd=i:n.next=i,i===null&&(id=n)):(n=r,(e!==0||a&3)&&(od=!0)),r=i}lu!==0&&lu!==5||ud(e,!1),cd!==0&&(cd=0)}function pd(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&Wd(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function kf(e,t,n){var r=Of;if(r&&typeof t==`string`&&t){var i=Pt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),Cf.has(i)||(Cf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Hd(t,`link`,e),ht(t),r.head.appendChild(t)))}}function Af(e){Tf.D(e),kf(`dns-prefetch`,e,null)}function jf(e,t){Tf.C(e,t),kf(`preconnect`,e,t)}function Mf(e,t,n){Tf.L(e,t,n);var r=Of;if(r&&e&&t){var i=`link[rel="preload"][as="`+Pt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Pt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Pt(n.imageSizes)+`"]`)):i+=`[href="`+Pt(e)+`"]`;var a=i;switch(t){case`style`:a=Rf(e);break;case`script`:a=Hf(e)}Sf.has(a)||(e=m({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),Sf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(zf(a))||t===`script`&&r.querySelector(Uf(a))||(t=r.createElement(`link`),Hd(t,`link`,e),ht(t),r.head.appendChild(t)))}}function Nf(e,t){Tf.m(e,t);var n=Of;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Pt(r)+`"][href="`+Pt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Hf(e)}if(!Sf.has(a)&&(e=m({rel:`modulepreload`,href:e},t),Sf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Uf(a)))return}r=n.createElement(`link`),Hd(r,`link`,e),ht(r),n.head.appendChild(r)}}}function Pf(e,t,n){Tf.S(e,t,n);var r=Of;if(r&&e){var i=mt(r).hoistableStyles,a=Rf(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(zf(a)))s.loading=5;else{e=m({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=Sf.get(a))&&Kf(e,n);var c=o=r.createElement(`link`);ht(c),Hd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Gf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Ff(e,t){Tf.X(e,t);var n=Of;if(n&&e){var r=mt(n).hoistableScripts,i=Hf(e),a=r.get(i);a||(a=n.querySelector(Uf(i)),a||(e=m({src:e,async:!0},t),(t=Sf.get(i))&&qf(e,t),a=n.createElement(`script`),ht(a),Hd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function If(e,t){Tf.M(e,t);var n=Of;if(n&&e){var r=mt(n).hoistableScripts,i=Hf(e),a=r.get(i);a||(a=n.querySelector(Uf(i)),a||(e=m({src:e,async:!0,type:`module`},t),(t=Sf.get(i))&&qf(e,t),a=n.createElement(`script`),ht(a),Hd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Lf(e,t,n,r){var a=(a=re.current)?wf(a):null;if(!a)throw Error(i(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=Rf(n.href),n=mt(a).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Rf(n.href);var o=mt(a).hoistableStyles,s=o.get(e);if(s||(a=a.ownerDocument||a,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=a.querySelector(zf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),Sf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},Sf.set(e,n),o||Vf(a,e,n,s.state))),t&&r===null)throw Error(i(528,``));return s}if(t&&r!==null)throw Error(i(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Hf(n),n=mt(a).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(i(444,e))}}function Rf(e){return`href="`+Pt(e)+`"`}function zf(e){return`link[rel="stylesheet"][`+e+`]`}function Bf(e){return m({},e,{"data-precedence":e.precedence,precedence:null})}function Vf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Hd(t,`link`,n),ht(t),e.head.appendChild(t))}function Hf(e){return`[src="`+Pt(e)+`"]`}function Uf(e){return`script[async]`+e}function Wf(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Pt(n.href)+`"]`);if(r)return t.instance=r,ht(r),r;var a=m({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),ht(r),Hd(r,`style`,a),Gf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=Rf(n.href);var o=e.querySelector(zf(a));if(o)return t.state.loading|=4,t.instance=o,ht(o),o;r=Bf(n),(a=Sf.get(a))&&Kf(r,a),o=(e.ownerDocument||e).createElement(`link`),ht(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Hd(o,`link`,r),t.state.loading|=4,Gf(o,n.precedence,e),t.instance=o;case`script`:return o=Hf(n.src),(a=e.querySelector(Uf(o)))?(t.instance=a,ht(a),a):(r=n,(a=Sf.get(o))&&(r=m({},n),qf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),ht(a),Hd(a,`link`,r),e.head.appendChild(a),t.instance=a);case`void`:return null;default:throw Error(i(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Gf(r,n.precedence,e));return t.instance}function Gf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function Zf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Qf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function $f(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Rf(r.href),a=t.querySelector(zf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=np.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,ht(a);return}a=t.ownerDocument||t,r=Bf(r),(i=Sf.get(i))&&Kf(r,i),a=a.createElement(`link`),ht(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Hd(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=np.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var ep=0;function tp(e,t){return e.stylesheets&&e.count===0&&ip(e,e.stylesheets),0ep?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function np(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)ip(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var rp=null;function ip(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,rp=new Map,t.forEach(ap,e),rp=null,np.call(e))}function ap(e,t){if(!(t.state.loading&4)){var n=rp.get(e);if(n)var r=n.get(null);else{n=new Map,rp.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=g()})),v=l(d(),1),y=_(),b=`modulepreload`,x=function(e){return`/`+e},S={},C=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=x(t,n),t=s(t),t in S)return;S[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:b,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},w=/^(?:[a-z][a-z0-9+.-]*:|[\\/]{2})/i,T=/^[\\/]{2}/;function E(e,t){return t+e.replace(/\\/g,`/`)}var D=`popstate`;function O(e){return typeof e==`object`&&!!e&&`pathname`in e&&`search`in e&&`hash`in e&&`state`in e&&`key`in e}function k(e={}){function t(e,t){let n=t.state?.masked,{pathname:r,search:i,hash:a}=n||e.location;return P(``,{pathname:r,search:i,hash:a},t.state&&t.state.usr||null,t.state&&t.state.key||`default`,n?{pathname:e.location.pathname,search:e.location.search,hash:e.location.hash}:void 0)}function n(e,t){return typeof t==`string`?t:F(t)}return L(t,n,null,e)}function A(e,t){if(e===!1||e==null)throw Error(t)}function j(e,t){if(!e){typeof console<`u`&&console.warn(t);try{throw Error(t)}catch{}}}function M(){return Math.random().toString(36).substring(2,10)}function N(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function P(e,t,n=null,r,i){return{pathname:typeof e==`string`?e:e.pathname,search:``,hash:``,...typeof t==`string`?I(t):t,state:n,key:t&&t.key||r||M(),mask:i}}function F({pathname:e=`/`,search:t=``,hash:n=``}){return t&&t!==`?`&&(e+=t.charAt(0)===`?`?t:`?`+t),n&&n!==`#`&&(e+=n.charAt(0)===`#`?n:`#`+n),e}function I(e){let t={};if(e){let n=e.indexOf(`#`);n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf(`?`);r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function L(e,t,n,r={}){let{window:i=document.defaultView,v5Compat:a=!1}=r,o=i.history,s=`POP`,c=null,l=u();l??(l=0,o.replaceState({...o.state,idx:l},``));function u(){return(o.state||{idx:null}).idx}function d(){s=`POP`;let e=u(),t=e==null?null:e-l;l=e,c&&c({action:s,location:h.location,delta:t})}function f(e,t){s=`PUSH`;let r=O(e)?e:P(h.location,e,t);n&&n(r,e),l=u()+1;let d=N(r,l),f=h.createHref(r.mask||r);try{o.pushState(d,``,f)}catch(e){if(e instanceof DOMException&&e.name===`DataCloneError`)throw e;i.location.assign(f)}a&&c&&c({action:s,location:h.location,delta:1})}function p(e,t){s=`REPLACE`;let r=O(e)?e:P(h.location,e,t);n&&n(r,e),l=u();let i=N(r,l),d=h.createHref(r.mask||r);o.replaceState(i,``,d),a&&c&&c({action:s,location:h.location,delta:0})}function m(e){return R(i,e)}let h={get action(){return s},get location(){return e(i,o)},listen(e){if(c)throw Error(`A history only accepts one active listener`);return i.addEventListener(D,d),c=e,()=>{i.removeEventListener(D,d),c=null}},createHref(e){return t(i,e)},createURL:m,encodeLocation(e){let t=m(e);return{pathname:t.pathname,search:t.search,hash:t.hash}},push:f,replace:p,go(e){return o.go(e)}};return h}function R(e,t,n=!1){let r=`http://localhost`;e&&(r=e.location.origin===`null`?e.location.href:e.location.origin),A(r,`No window.location.(origin|href) available to create URL`);let i=typeof t==`string`?t:F(t);return i=i.replace(/ $/,`%20`),!n&&T.test(i)&&(i=r+i),new URL(i,r)}function z(e,t,n=`/`){return B(e,t,n,!1)}function B(e,t,n,r,i){let a=ge((typeof t==`string`?I(t):t).pathname||`/`,n);if(a==null)return null;let o=i??V(e),s=null,c=he(a);for(let e=0;s==null&&e{let c={relativePath:s===void 0?e.path||``:s,caseSensitive:e.caseSensitive===!0,childrenIndex:a,route:e};if(c.relativePath.startsWith(`/`)){if(!c.relativePath.startsWith(r)&&o)return;A(c.relativePath.startsWith(r),`Absolute route path "${c.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),c.relativePath=c.relativePath.slice(r.length)}let l=we([r,c.relativePath]),u=n.concat(c);e.children&&e.children.length>0&&(A(e.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${l}".`),H(e.children,t,u,l,o)),!(e.path==null&&!e.index)&&t.push({path:l,score:le(l,e.index),routesMeta:u.map((e,t)=>{let[n,r]=me(e.relativePath,e.caseSensitive,t===u.length-1);return{...e,matcher:n,compiledParams:r}})})};return e.forEach((e,t)=>{if(e.path===``||!e.path?.includes(`?`))a(e,t);else for(let n of ee(e.path))a(e,t,!0,n)}),t}function ee(e){let t=e.split(`/`);if(t.length===0)return[];let[n,...r]=t,i=n.endsWith(`?`),a=n.replace(/\?$/,``);if(r.length===0)return i?[a,``]:[a];let o=ee(r.join(`/`)),s=[];return s.push(...o.map(e=>e===``?a:[a,e].join(`/`))),i&&s.push(...o),s.map(t=>e.startsWith(`/`)&&t===``?`/`:t)}function te(e){e.sort((e,t)=>e.score===t.score?ue(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var ne=/^:[\w-]+$/,re=3,ie=2,ae=1,oe=10,se=-2,ce=e=>e===`*`;function le(e,t){let n=e.split(`/`),r=n.length;return n.some(ce)&&(r+=se),t&&(r+=ie),n.filter(e=>!ce(e)).reduce((e,t)=>e+(ne.test(t)?re:t===``?ae:oe),r)}function ue(e,t){return e.length===t.length&&e.slice(0,-1).every((e,n)=>e===t[n])?e[e.length-1]-t[t.length-1]:0}function de(e,t,n=!1){let{routesMeta:r}=e,i={},a=`/`,o=[];for(let e=0;e{if(t===`*`){let e=s[r]||``;o=a.slice(0,a.length-e.length).replace(/(.)\/+$/,`$1`)}let i=s[r];return e[t]=n&&!i?void 0:(i||``).replace(/%2F/g,`/`),e},{}),pathname:a,pathnameBase:o,pattern:e}}function me(e,t=!1,n=!0){j(e===`*`||!e.endsWith(`*`)||e.endsWith(`/*`),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,`/*`)}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,`/*`)}".`);let r=[],i=`^`+e.replace(/\/*\*?$/,``).replace(/^\/*/,`/`).replace(/[\\.*+^${}|()[\]]/g,`\\$&`).replace(/\/:([\w-]+)(\?)?/g,(e,t,n,i,a)=>{if(r.push({paramName:t,isOptional:n!=null}),n){let t=a.charAt(i+e.length);return t&&t!==`/`?`/([^\\/]*)`:`(?:/([^\\/]*))?`}return`/([^\\/]+)`}).replace(/\/([\w-]+)\?(\/|$)/g,`(/$1)?$2`);return e.endsWith(`*`)?(r.push({paramName:`*`}),i+=e===`*`||e===`/*`?`(.*)$`:`(?:\\/(.+)|\\/*)$`):n?i+=`\\/*$`:e!==``&&e!==`/`&&(i+=`(?:(?=\\/|$))`),[new RegExp(i,t?void 0:`i`),r]}function he(e){try{return e.split(`/`).map(e=>decodeURIComponent(e).replace(/\//g,`%2F`)).join(`/`)}catch(t){return j(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function ge(e,t){if(t===`/`)return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith(`/`)?t.length-1:t.length,r=e.charAt(n);return r&&r!==`/`?null:e.slice(n)||`/`}function _e(e,t=`/`){let{pathname:n,search:r=``,hash:i=``}=typeof e==`string`?I(e):e,a;return n?(n=Ce(n),a=n.startsWith(`/`)?ve(n.substring(1),`/`):ve(n,t)):a=t,{pathname:a,search:De(r),hash:Oe(i)}}function ve(e,t){let n=Te(t).split(`/`);return e.split(`/`).forEach(e=>{e===`..`?n.length>1&&n.pop():e!==`.`&&n.push(e)}),n.length>1?n.join(`/`):`/`}function ye(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function be(e){return e.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function xe(e){let t=be(e);return t.map((e,n)=>n===t.length-1?e.pathname:e.pathnameBase)}function Se(e,t,n,r=!1){let i;typeof e==`string`?i=I(e):(i={...e},A(!i.pathname||!i.pathname.includes(`?`),ye(`?`,`pathname`,`search`,i)),A(!i.pathname||!i.pathname.includes(`#`),ye(`#`,`pathname`,`hash`,i)),A(!i.search||!i.search.includes(`#`),ye(`#`,`search`,`hash`,i)));let a=e===``||i.pathname===``,o=a?`/`:i.pathname,s;if(o==null)s=n;else{let e=t.length-1;if(!r&&o.startsWith(`..`)){let t=o.split(`/`);for(;t[0]===`..`;)t.shift(),--e;i.pathname=t.join(`/`)}s=e>=0?t[e]:`/`}let c=_e(i,s),l=o&&o!==`/`&&o.endsWith(`/`),u=(a||o===`.`)&&n.endsWith(`/`);return!c.pathname.endsWith(`/`)&&(l||u)&&(c.pathname+=`/`),c}var Ce=e=>e.replace(/[\\/]{2,}/g,`/`),we=e=>Ce(e.join(`/`)),Te=e=>e.replace(/\/+$/,``),Ee=e=>Te(e).replace(/^\/*/,`/`),De=e=>!e||e===`?`?``:e.startsWith(`?`)?e:`?`+e,Oe=e=>!e||e===`#`?``:e.startsWith(`#`)?e:`#`+e,ke=class{constructor(e,t,n,r=!1){this.status=e,this.statusText=t||``,this.internal=r,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function Ae(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.internal==`boolean`&&`data`in e}function je(e){return we(e.map(e=>e.route.path).filter(Boolean))||`/`}var Me=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;function Ne(e,t){let n=e;if(typeof n!=`string`||!w.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,i=!1;if(Me)try{let e=new URL(window.location.href),r=T.test(n)?new URL(E(n,e.protocol)):new URL(n),a=ge(r.pathname,t);r.origin===e.origin&&a!=null?n=a+r.search+r.hash:i=!0}catch{j(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:i,to:n}}Object.getOwnPropertyNames(Object.prototype).sort().join(`\0`);var Pe=[`POST`,`PUT`,`PATCH`,`DELETE`];new Set(Pe);var Fe=[`GET`,...Pe];new Set(Fe);var Ie=[`about:`,`blob:`,`chrome:`,`chrome-untrusted:`,`content:`,`data:`,`devtools:`,`file:`,`filesystem:`,`javascript:`];function Le(e){try{return Ie.includes(new URL(e).protocol)}catch{return!1}}var Re=v.createContext(null);Re.displayName=`DataRouter`;var ze=v.createContext(null);ze.displayName=`DataRouterState`;var Be=v.createContext(!1);function Ve(){return v.useContext(Be)}var He=v.createContext({isTransitioning:!1});He.displayName=`ViewTransition`;var Ue=v.createContext(new Map);Ue.displayName=`Fetchers`;var We=v.createContext(null);We.displayName=`Await`;var Ge=v.createContext(null);Ge.displayName=`Navigation`;var Ke=v.createContext(null);Ke.displayName=`Location`;var qe=v.createContext({outlet:null,matches:[],isDataRoute:!1});qe.displayName=`Route`;var Je=v.createContext(null);Je.displayName=`RouteError`;var Ye=`REACT_ROUTER_ERROR`,Xe=`REDIRECT`,Ze=`ROUTE_ERROR_RESPONSE`;function Qe(e){if(e.startsWith(`${Ye}:${Xe}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`&&typeof t.location==`string`&&typeof t.reloadDocument==`boolean`&&typeof t.replace==`boolean`)return t}catch{}}function $e(e){if(e.startsWith(`${Ye}:${Ze}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`)return new ke(t.status,t.statusText,t.data)}catch{}}function et(e,{relative:t}={}){A(tt(),`useHref() may be used only in the context of a component.`);let{basename:n,navigator:r}=v.useContext(Ge),{hash:i,pathname:a,search:o}=lt(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:we([n,a])),r.createHref({pathname:s,search:o,hash:i})}function tt(){return v.useContext(Ke)!=null}function nt(){return A(tt(),`useLocation() may be used only in the context of a component.`),v.useContext(Ke).location}var rt=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function it(e){v.useContext(Ge).static||v.useLayoutEffect(e)}function at(){let{isDataRoute:e}=v.useContext(qe);return e?Et():ot()}function ot(){A(tt(),`useNavigate() may be used only in the context of a component.`);let e=v.useContext(Re),{basename:t,navigator:n}=v.useContext(Ge),{matches:r}=v.useContext(qe),{pathname:i}=nt(),a=JSON.stringify(xe(r)),o=v.useRef(!1);return it(()=>{o.current=!0}),v.useCallback((r,s={})=>{if(j(o.current,rt),!o.current)return;if(typeof r==`number`){n.go(r);return}let c=Se(r,JSON.parse(a),i,s.relative===`path`);e==null&&t!==`/`&&(c.pathname=c.pathname===`/`?t:we([t,c.pathname])),(s.replace?n.replace:n.push)(c,s.state,s)},[t,n,a,i,e])}var st=v.createContext(null);function ct(e){let t=v.useContext(qe).outlet;return v.useMemo(()=>t&&v.createElement(st.Provider,{value:e},t),[t,e])}function lt(e,{relative:t}={}){let{matches:n}=v.useContext(qe),{pathname:r}=nt(),i=JSON.stringify(xe(n));return v.useMemo(()=>Se(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function ut(e,t){return dt(e,t)}function dt(e,t,n){A(tt(),`useRoutes() may be used only in the context of a component.`);let{navigator:r}=v.useContext(Ge),{matches:i}=v.useContext(qe),a=i[i.length-1],o=a?a.params:{},s=a?a.pathname:`/`,c=a?a.pathnameBase:`/`,l=a&&a.route;{let e=l&&l.path||``;Ot(s,!l||e.endsWith(`*`)||e.endsWith(`*?`),`You rendered descendant (or called \`useRoutes()\`) at "${s}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. -Please change the parent to .`)}let u=nt(),d;if(t){let e=typeof t==`string`?I(t):t;A(c===`/`||e.pathname?.startsWith(c),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${c}" but pathname "${e.pathname}" was given in the \`location\` prop.`),d=e}else d=u;let f=d.pathname||`/`,p=f;if(c!==`/`){let e=c.replace(/^\//,``).split(`/`);p=`/`+f.replace(/^\//,``).split(`/`).slice(e.length).join(`/`)}let m=n&&n.state.matches.length?n.state.matches.map(e=>Object.assign(e,{route:n.manifest[e.route.id]||e.route})):z(e,{pathname:p});j(l||m!=null,`No routes matched location "${d.pathname}${d.search}${d.hash}" `),j(m==null||m[m.length-1].route.element!==void 0||m[m.length-1].route.Component!==void 0||m[m.length-1].route.lazy!==void 0,`Matched leaf route at location "${d.pathname}${d.search}${d.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let h=vt(m&&m.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:we([c,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?c:we([c,r.encodeLocation?r.encodeLocation(e.pathnameBase.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathnameBase])})),i,n);return t&&h?v.createElement(Ke.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,mask:void 0,...d},navigationType:`POP`}},h):h}function ft(){let e=Tt(),t=Ae(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,r=`rgba(200,200,200, 0.5)`,i={padding:`0.5rem`,backgroundColor:r},a={padding:`2px 4px`,backgroundColor:r},o=null;return console.error(`Error handled by React Router default ErrorBoundary:`,e),o=v.createElement(v.Fragment,null,v.createElement(`p`,null,`💿 Hey developer 👋`),v.createElement(`p`,null,`You can provide a way better UX than this when your app throws errors by providing your own `,v.createElement(`code`,{style:a},`ErrorBoundary`),` or`,` `,v.createElement(`code`,{style:a},`errorElement`),` prop on your route.`)),v.createElement(v.Fragment,null,v.createElement(`h2`,null,`Unexpected Application Error!`),v.createElement(`h3`,{style:{fontStyle:`italic`}},t),n?v.createElement(`pre`,{style:i},n):null,o)}var pt=v.createElement(ft,null),mt=class extends v.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!==`idle`&&e.revalidation===`idle`?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error===void 0?t.error:e.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){this.props.onError?this.props.onError(e,t):console.error(`React Router caught the following error during render`,e)}render(){let e=this.state.error;if(this.context&&typeof e==`object`&&e&&`digest`in e&&typeof e.digest==`string`){let t=$e(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:v.createElement(qe.Provider,{value:this.props.routeContext},v.createElement(Je.Provider,{value:e,children:this.props.component}));return this.context?v.createElement(gt,{error:e},t):t}};mt.contextType=Be;var ht=new WeakMap;function gt({children:e,error:t}){let{basename:n}=v.useContext(Ge);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=Qe(t.digest);if(e){let r=ht.get(t);if(r)throw r;let i=Ne(e.location,n),a=i.absoluteURL||i.to;if(Le(a))throw Error(`Invalid redirect location`);if(Me&&!ht.get(t)){if(i.isExternal||e.reloadDocument)window.location.href=a;else{let n=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(i.to,{replace:e.replace}));throw ht.set(t,n),n}}return v.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${a}`})}}return e}function _t({routeContext:e,match:t,children:n}){let r=v.useContext(Re);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),v.createElement(qe.Provider,{value:e},n)}function vt(e,t=[],n){let r=n?.state;if(e==null){if(!r)return null;if(r.errors)e=r.matches;else if(t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let i=e,a=r?.errors;if(a!=null){let e=i.findIndex(e=>e.route.id&&a?.[e.route.id]!==void 0);A(e>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(a).join(`,`)}`),i=i.slice(0,Math.min(i.length,e+1))}let o=!1,s=-1;if(n&&r){o=r.renderFallback;for(let e=0;e=0?i.slice(0,s+1):[i[0]];break}}}}let c=n?.onError,l=r&&c?(e,t)=>{c(e,{location:r.location,params:r.matches?.[0]?.params??{},pattern:je(r.matches),errorInfo:t})}:void 0;return i.reduceRight((e,n,c)=>{let u,d=!1,f=null,p=null;r&&(u=a&&n.route.id?a[n.route.id]:void 0,f=n.route.errorElement||pt,o&&(s<0&&c===0?(Ot(`route-fallback`,!1,"No `HydrateFallback` element provided to render during initial hydration"),d=!0,p=null):s===c&&(d=!0,p=n.route.hydrateFallbackElement||null)));let m=t.concat(i.slice(0,c+1)),h=()=>{let t;return t=u?f:d?p:n.route.Component?v.createElement(n.route.Component,null):n.route.element?n.route.element:e,v.createElement(_t,{match:n,routeContext:{outlet:e,matches:m,isDataRoute:r!=null},children:t})};return r&&(n.route.ErrorBoundary||n.route.errorElement||c===0)?v.createElement(mt,{location:r.location,revalidation:r.revalidation,component:f,error:u,children:h(),routeContext:{outlet:null,matches:m,isDataRoute:!0},onError:l}):h()},null)}function yt(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function bt(e){let t=v.useContext(Re);return A(t,yt(e)),t}function xt(e){let t=v.useContext(ze);return A(t,yt(e)),t}function St(e){let t=v.useContext(qe);return A(t,yt(e)),t}function Ct(e){let t=St(e),n=t.matches[t.matches.length-1];return A(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function wt(){return Ct(`useRouteId`)}function Tt(){let e=v.useContext(Je),t=xt(`useRouteError`),n=Ct(`useRouteError`);return e===void 0?t.errors?.[n]:e}function Et(){let{router:e}=bt(`useNavigate`),t=Ct(`useNavigate`),n=v.useRef(!1);return it(()=>{n.current=!0}),v.useCallback(async(r,i={})=>{j(n.current,rt),n.current&&(typeof r==`number`?await e.navigate(r):await e.navigate(r,{fromRouteId:t,...i}))},[e,t])}var Dt={};function Ot(e,t,n){!t&&!Dt[e]&&(Dt[e]=!0,j(!1,n))}v.memo(kt);function kt({routes:e,manifest:t,future:n,state:r,isStatic:i,onError:a}){return dt(e,void 0,{manifest:t,state:r,isStatic:i,onError:a,future:n})}function At({to:e,replace:t,state:n,relative:r}){A(tt(),` may be used only in the context of a component.`);let{static:i}=v.useContext(Ge);j(!i,` must not be used on the initial render in a . This is a no-op, but you should modify your code so the is only ever rendered in response to some user interaction or state change.`);let{matches:a}=v.useContext(qe),{pathname:o}=nt(),s=at(),c=Se(e,xe(a),o,r===`path`),l=JSON.stringify(c);return v.useEffect(()=>{s(JSON.parse(l),{replace:t,state:n,relative:r})},[s,l,r,t,n]),null}function jt(e){return ct(e.context)}function Mt(e){A(!1,`A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .`)}function Nt({basename:e=`/`,children:t=null,location:n,navigationType:r=`POP`,navigator:i,static:a=!1,useTransitions:o}){A(!tt(),`You cannot render a inside another . You should never have more than one in your app.`);let s=e.replace(/^\/*/,`/`),c=v.useMemo(()=>({basename:s,navigator:i,static:a,useTransitions:o,future:{}}),[s,i,a,o]);typeof n==`string`&&(n=I(n));let{pathname:l=`/`,search:u=``,hash:d=``,state:f=null,key:p=`default`,mask:m}=n,h=v.useMemo(()=>{let e=ge(l,s);return e==null?null:{location:{pathname:e,search:u,hash:d,state:f,key:p,mask:m},navigationType:r}},[s,l,u,d,f,p,r,m]);return j(h!=null,` is not able to match the URL "${l}${u}${d}" because it does not start with the basename, so the won't render anything.`),h==null?null:v.createElement(Ge.Provider,{value:c},v.createElement(Ke.Provider,{children:t,value:h}))}function Pt({children:e,location:t}){return ut(Ft(e),t)}v.Component;function Ft(e,t=[]){let n=[];return v.Children.forEach(e,(e,r)=>{if(!v.isValidElement(e))return;let i=[...t,r];if(e.type===v.Fragment){n.push.apply(n,Ft(e.props.children,i));return}A(e.type===Mt,`[${typeof e.type==`string`?e.type:e.type.name}] is not a component. All component children of must be a or `),A(!e.props.index||!e.props.children,`An index route cannot have child routes.`);let a={id:e.props.id||i.join(`-`),caseSensitive:e.props.caseSensitive,element:e.props.element,Component:e.props.Component,index:e.props.index,path:e.props.path,middleware:e.props.middleware,loader:e.props.loader,action:e.props.action,hydrateFallbackElement:e.props.hydrateFallbackElement,HydrateFallback:e.props.HydrateFallback,errorElement:e.props.errorElement,ErrorBoundary:e.props.ErrorBoundary,hasErrorBoundary:e.props.hasErrorBoundary===!0||e.props.ErrorBoundary!=null||e.props.errorElement!=null,shouldRevalidate:e.props.shouldRevalidate,handle:e.props.handle,lazy:e.props.lazy};e.props.children&&(a.children=Ft(e.props.children,i)),n.push(a)}),n}var It=`get`,Lt=`application/x-www-form-urlencoded`;function Rt(e){return typeof HTMLElement<`u`&&e instanceof HTMLElement}function zt(e){return Rt(e)&&e.tagName.toLowerCase()===`button`}function Bt(e){return Rt(e)&&e.tagName.toLowerCase()===`form`}function Vt(e){return Rt(e)&&e.tagName.toLowerCase()===`input`}function Ht(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function Ut(e,t){return e.button===0&&(!t||t===`_self`)&&!Ht(e)}function Wt(e=``){return new URLSearchParams(typeof e==`string`||Array.isArray(e)||e instanceof URLSearchParams?e:Object.keys(e).reduce((t,n)=>{let r=e[n];return t.concat(Array.isArray(r)?r.map(e=>[n,e]):[[n,r]])},[]))}function Gt(e,t){let n=Wt(e);return t&&t.forEach((e,r)=>{n.has(r)||t.getAll(r).forEach(e=>{n.append(r,e)})}),n}var Kt=null;function qt(){if(Kt===null)try{new FormData(document.createElement(`form`),0),Kt=!1}catch{Kt=!0}return Kt}var Jt=new Set([`application/x-www-form-urlencoded`,`multipart/form-data`,`text/plain`]);function Yt(e){return e!=null&&!Jt.has(e)?(j(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${Lt}"`),null):e}function Xt(e,t){let n,r,i,a,o;if(Bt(e)){let o=e.getAttribute(`action`);r=o?ge(o,t):null,n=e.getAttribute(`method`)||It,i=Yt(e.getAttribute(`enctype`))||Lt,a=new FormData(e)}else if(zt(e)||Vt(e)&&(e.type===`submit`||e.type===`image`)){let o=e.form;if(o==null)throw Error(`Cannot submit a