From 86c02761565ec1cacf5ab65cd0f1b550da4e176e Mon Sep 17 00:00:00 2001
From: cneicy
Date: Tue, 27 Oct 2026 20:43:39 +0800
Subject: [PATCH] feat: publish sanitized custom.97 source
---
.agents/rules/billing.md | 6 +-
.github/dependabot.yml | 6 +
.github/workflows/ci.yml | 17 +-
.github/workflows/electron-build.yml | 6 +-
.github/workflows/release.yml | 14 +-
AGENTS.md | 17 +-
CUSTOMIZATION.md | 15 +-
Dockerfile | 9 +-
Dockerfile.affinity-switch | 3 -
Dockerfile.browser-performance | 4 -
Dockerfile.candy-test | 3 -
Dockerfile.ccswitch | 3 -
Dockerfile.combined | 3 -
Dockerfile.compact-uptime | 3 -
Dockerfile.deployed | 3 -
Dockerfile.dev | 7 +-
Dockerfile.downgrade-rate | 3 -
Dockerfile.fingerprint-color | 3 -
Dockerfile.fingerprint-runtime | 4 -
Dockerfile.group-degradation | 3 -
Dockerfile.group-pricing | 24 -
Dockerfile.local-release | 6 +-
Dockerfile.meow | 9 -
Dockerfile.modeltrace | 10 -
Dockerfile.public-pelican | 3 -
Dockerfile.request-status | 47 -
Dockerfile.stream-health | 3 -
README.en.md | 500 +++---
README.fr.md | 474 ++----
README.ja.md | 474 ++----
README.md | 489 ++----
README.zh_CN.md | 491 +++---
README.zh_TW.md | 476 ++----
THIRD-PARTY-LICENSES.md | 309 ++--
VERSION | 2 +-
assets/screenshots/dashboard.en.jpg | Bin 0 -> 147327 bytes
assets/screenshots/dashboard.zh-CN.jpg | Bin 0 -> 141385 bytes
assets/screenshots/models.en.jpg | Bin 0 -> 164228 bytes
assets/screenshots/models.zh-CN.jpg | Bin 0 -> 157549 bytes
assets/screenshots/plugin-marketplace.en.jpg | Bin 0 -> 163158 bytes
.../screenshots/plugin-marketplace.zh-CN.jpg | Bin 0 -> 143022 bytes
assets/screenshots/usage-logs.en.jpg | Bin 0 -> 236390 bytes
assets/screenshots/usage-logs.zh-CN.jpg | Bin 0 -> 218658 bytes
bin/migration_v0.2-v0.3.sql | 6 -
bin/migration_v0.3-v0.4.sql | 17 -
bin/time_test.sh | 40 -
build-fingerprint-backend.sh | 9 -
common/audio.go | 4 +-
common/body_storage.go | 2 +-
common/constants.go | 13 +
common/container_usage.go | 174 +++
common/email.go | 4 +-
common/endpoint_defaults.go | 2 +
common/endpoint_type.go | 7 +
common/env.go | 5 +-
common/gin.go | 108 +-
common/gopool.go | 3 +-
common/init.go | 20 +-
common/limiter/limiter.go | 2 +-
common/log_text.go | 35 +
common/log_text.zh-CN.json | 854 ++++++++++
common/message.go | 50 +
common/model.go | 20 -
common/model_test.go | 10 +
common/pprof.go | 8 +-
common/quota.go | 5 -
common/redis.go | 34 +-
common/str.go | 4 +-
common/sys_log.go | 8 +-
common/system_monitor.go | 113 +-
common/system_monitor_test.go | 280 ++++
common/topup-ratio.go | 4 +-
common/totp.go | 5 +-
common/utils.go | 25 -
constant/channel.go | 2 +-
constant/context_key.go | 5 +
constant/endpoint_type.go | 9 +
controller/access_token.go | 319 +++-
controller/access_token_audit_test.go | 499 +++++-
controller/admin_user_verification_test.go | 389 +++++
controller/astra_sampling.go | 40 +-
controller/audit.go | 17 +-
controller/auth_session.go | 9 +-
controller/billing_option_test.go | 146 ++
controller/bot_balance.go | 84 +
controller/bot_balance_test.go | 113 ++
controller/chain_detection.go | 156 ++
controller/channel-billing.go | 132 +-
controller/channel-test.go | 42 +-
controller/channel.go | 356 ++---
controller/channel_affinity_cache.go | 11 +-
controller/channel_billing_sub2api_test.go | 159 ++
controller/channel_billing_test.go | 49 +
controller/channel_priority.go | 40 +
controller/channel_task_plugin_bind_test.go | 66 +
.../channel_task_plugin_validation_test.go | 46 +
controller/channel_test_internal_test.go | 2 +-
controller/channel_test_request_test.go | 4 +
controller/channel_upstream_update.go | 141 +-
controller/channel_upstream_update_test.go | 17 +-
controller/checkin.go | 26 +-
controller/ciallo_detection.go | 118 ++
controller/codex_usage.go | 27 +-
controller/custom_oauth.go | 90 +-
controller/detection.go | 30 +-
controller/detection_artwork.go | 41 +
controller/detection_test.go | 460 +++++-
controller/detection_worker.go | 99 +-
controller/email_binding.go | 16 +-
controller/group.go | 8 +-
controller/group_status_events.go | 117 ++
controller/group_status_events_test.go | 74 +
controller/log.go | 32 +-
controller/log_export_test.go | 246 +++
controller/login_verification.go | 9 +-
controller/manxue_artwork_url.go | 27 -
controller/midjourney.go | 38 +-
controller/misc.go | 59 +-
controller/model.go | 2 +-
controller/model_management_test.go | 13 +-
controller/model_meta.go | 10 +-
...model_pricing_conversion_endpoints_test.go | 71 +
controller/model_sync.go | 2 +-
controller/oauth.go | 66 +-
controller/option.go | 142 +-
controller/passkey.go | 119 +-
controller/passkey_test.go | 34 +-
controller/payment_compliance.go | 10 +-
.../payment_webhook_availability_test.go | 116 ++
controller/perf_metrics.go | 8 +-
controller/performance.go | 24 +-
controller/playground.go | 6 +-
controller/plugin_protocol.go | 97 +-
controller/plugin_protocol_image.go | 260 ++++
controller/plugin_protocol_test.go | 288 ++++
controller/prefill_group.go | 8 +-
controller/pricing.go | 15 +-
controller/ratio_config.go | 6 +-
controller/ratio_sync.go | 32 +-
controller/redemption.go | 40 +-
controller/relay.go | 67 +-
controller/relay_client_error_test.go | 8 +-
controller/relay_log_privacy_test.go | 2 +-
controller/relay_task_plugin_test.go | 97 +-
controller/responses_websocket.go | 7 +-
controller/responses_websocket_test.go | 130 +-
controller/secure_verification.go | 40 +-
controller/security_account_test.go | 67 +-
controller/security_enrollment_test.go | 455 ++++--
controller/setup.go | 50 +-
controller/status_page.go | 4 +
controller/subscription.go | 110 +-
controller/subscription_payment_creem.go | 21 +-
controller/subscription_payment_epay.go | 20 +-
controller/subscription_payment_stripe.go | 20 +-
.../subscription_payment_waffo_pancake.go | 22 +-
controller/system_task_handlers.go | 3 +-
controller/task.go | 48 +-
controller/task_generic_test.go | 156 +-
controller/task_plugin.go | 58 +-
controller/task_plugin_test.go | 162 +-
controller/theme_compat_test.go | 2 +-
controller/token.go | 59 +-
controller/token_test.go | 123 ++
controller/topup.go | 95 +-
controller/topup_creem.go | 94 +-
controller/topup_quota_limit_test.go | 10 +-
controller/topup_stripe.go | 92 +-
controller/topup_waffo.go | 70 +-
controller/topup_waffo_pancake.go | 110 +-
controller/twofa.go | 70 +-
controller/uptime_kuma.go | 74 +-
controller/uptime_kuma_test.go | 4 +-
controller/usedata.go | 16 +-
controller/user.go | 301 ++--
controller/user_manage_test.go | 140 +-
controller/user_quota.go | 9 +-
controller/vendor_meta.go | 2 +-
controller/video_proxy.go | 29 +-
controller/vision_detection.go | 105 ++
controller/wechat.go | 64 +-
docker-compose.yml | 1 +
docs/authentication.md | 31 +-
docs/group-status-events.md | 9 +
docs/plugin-api/README.md | 10 +-
docs/plugin-api/v1.d.ts | 34 +-
docs/plugin-api/v1.md | 221 ++-
docs/plugin-api/v1.schema.json | 23 +-
docs/token-routing-custom18.md | 2 +-
dto/task.go | 49 +-
dto/task_plugin.go | 18 +-
electron/build.sh | 8 +-
electron/icon.png | Bin 31262 -> 37292 bytes
electron/package-lock.json | 301 ++--
electron/package.json | 2 +-
electron/tray-icon-windows.png | Bin 1203 -> 1333 bytes
electron/tray-iconTemplate.png | Bin 459 -> 368 bytes
electron/tray-iconTemplate@2x.png | Bin 754 -> 935 bytes
go.mod | 29 +-
go.sum | 39 +-
i18n/i18n.go | 90 +-
i18n/i18n_test.go | 207 +++
i18n/keys.go | 410 ++---
i18n/locales/en.yaml | 342 +----
i18n/locales/zh-CN.yaml | 342 +----
i18n/locales/zh-TW.yaml | 342 +----
logger/logger.go | 22 +-
main.go | 54 +-
makefile | 2 +
middleware/access_token_routes.go | 370 +++++
middleware/audit.go | 23 +-
middleware/auth.go | 289 +++-
middleware/auth_test.go | 202 ++-
middleware/distributor.go | 63 +-
middleware/distributor_test.go | 80 +
middleware/email-verification-rate-limit.go | 11 +-
middleware/frontend_static.go | 109 ++
middleware/frontend_static_test.go | 106 ++
middleware/header_nav.go | 5 +
middleware/header_nav_test.go | 78 +-
middleware/i18n.go | 23 +-
middleware/model-rate-limit.go | 37 +-
middleware/model_rate_limit_test.go | 109 +-
middleware/performance.go | 34 +-
middleware/rate-limit.go | 26 +-
middleware/rate_limit_test.go | 69 +-
middleware/recover.go | 4 +-
middleware/request-id.go | 4 +
middleware/secure_verification.go | 20 +-
middleware/task_plugin.go | 239 +--
middleware/task_plugin_test.go | 252 ++-
middleware/trusted_proxies.go | 2 +-
middleware/turnstile-check.go | 10 +-
middleware/utils.go | 13 +-
model/ability.go | 13 +-
model/audit_log.go | 16 +-
model/auth_flow.go | 4 +
model/bot_balance.go | 20 +
model/channel.go | 43 +-
model/channel_cache.go | 14 +-
model/channel_constraint.go | 13 +-
model/channel_priority.go | 244 +++
model/channel_priority_test.go | 209 +++
model/checkin.go | 13 +-
model/detection.go | 27 +-
model/detection_schedule.go | 7 +-
model/detection_schedule_test.go | 12 +-
model/frontend_option_migration.go | 6 +-
model/gorm_logger.go | 2 +-
model/group_model_performance.go | 179 +++
model/group_model_performance_test.go | 116 ++
model/log.go | 83 +-
model/log_other.go | 25 +-
model/log_routing_group_test.go | 16 +-
model/main.go | 96 +-
model/model_pricing_config.go | 31 +-
model/model_pricing_conversion.go | 65 +-
model/option.go | 20 +-
model/option_primary_key_migration.go | 6 +-
model/passkey.go | 27 +-
model/perf_metric.go | 19 +
model/pricing.go | 5 +-
model/pricing_usage_schema_test.go | 13 +
model/quota_reserve.go | 13 +-
model/redemption.go | 23 +-
model/relay_attempt_test.go | 27 +
model/subscription.go | 76 +-
model/subscription_auth_test.go | 3 +-
model/subscription_reset_test.go | 2 +-
model/task.go | 36 +-
model/task_cas_test.go | 2 +
model/task_model_alias.go | 9 +-
model/task_plugin.go | 94 +-
model/task_plugin_channel_select_test.go | 57 +
model/token.go | 54 +-
model/token_auto_groups_cache_test.go | 28 +
model/token_routing.go | 14 +-
model/topup.go | 107 +-
model/twofa.go | 14 +-
model/usedata.go | 6 +-
model/user.go | 170 +-
model/user_access_token.go | 329 ++++
model/user_authentication_test.go | 11 +-
model/user_cache.go | 6 +-
model/user_quota_adjustment.go | 3 +-
model/user_session.go | 14 +-
model/user_update_test.go | 253 ++-
model/utils.go | 9 +-
oauth/discord.go | 28 +-
oauth/generic.go | 39 +-
oauth/github.go | 37 +-
oauth/linuxdo.go | 25 +-
oauth/oidc.go | 28 +-
oauth/registry.go | 6 +-
oauth/types.go | 36 +-
pkg/billingexpr/expr.md | 32 +-
pkg/billingexpr/types.go | 1 +
pkg/jsplugin/engine.go | 730 ++++++---
pkg/jsplugin/engine_test.go | 312 ++++
pkg/jsplugin/fetch.go | 292 ++++
pkg/jsplugin/json_state.go | 84 +-
pkg/jsplugin/registry.go | 112 +-
pkg/jsplugin/registry_test.go | 42 +
pkg/jsplugin/request.go | 26 +
pkg/jsplugin/routing.go | 128 +-
pkg/jsplugin/routing_test.go | 148 +-
pkg/jsplugin/utils.go | 245 ++-
pkg/model_fingerprint/response.go | 10 +-
pkg/perf_metrics/flush.go | 5 +-
pkg/perf_metrics/metrics.go | 116 +-
pkg/perf_metrics/metrics_test.go | 139 ++
pkg/perf_metrics/outcome.go | 14 +-
pkg/perf_metrics/types.go | 3 +
pkg/wsmanager/wsmanager.go | 6 +-
plugins/alibaba_responses_test.go | 434 +++++-
plugins/alibaba_wan3_test.go | 12 +-
plugins/builtin_plugins_test.go | 265 +++-
plugins/doubao_responses_test.go | 985 +++++++++++-
plugins/google_responses_test.go | 140 +-
plugins/hailuo_responses_test.go | 248 ++-
plugins/tasks/alibaba/plugin.js | 757 +++++++--
plugins/tasks/doubao/plugin.js | 851 +++++++++-
plugins/tasks/google/plugin.js | 100 +-
plugins/tasks/hailuo/plugin.js | 223 ++-
plugins/tasks/jimeng/plugin.js | 19 +-
plugins/tasks/kling/plugin.js | 27 +-
plugins/tasks/sora/plugin.js | 4 +-
plugins/tasks/sunoapi/plugin.js | 4 +-
plugins/tasks/vertex-ai/plugin.js | 105 +-
plugins/tasks/vidu/plugin.js | 105 +-
plugins/tasks/xai/plugin.js | 783 ++++++++++
plugins/vertex_ai_responses_test.go | 77 +
plugins/vidu_responses_test.go | 128 +-
plugins/xai_responses_test.go | 484 ++++++
relay/alpha_search_handler.go | 13 +-
relay/alpha_search_handler_test.go | 117 ++
relay/audio_handler.go | 2 +-
relay/channel/adapter.go | 3 +-
relay/channel/advancedcustom/adaptor.go | 23 +-
relay/channel/advancedcustom/adaptor_test.go | 136 ++
relay/channel/ali/adaptor.go | 116 +-
relay/channel/ali/adaptor_test.go | 356 ++---
relay/channel/ali/dto.go | 146 --
relay/channel/ali/image.go | 368 -----
relay/channel/ali/image_wan.go | 51 -
relay/channel/ali/web_search.go | 163 ++
relay/channel/api_request.go | 37 +-
relay/channel/aws/adaptor.go | 21 +-
relay/channel/aws/dto.go | 21 +-
relay/channel/aws/mantle.go | 144 ++
relay/channel/aws/relay-aws.go | 4 +-
relay/channel/aws/relay_aws_test.go | 458 ++++++
relay/channel/baidu/relay-baidu.go | 4 +-
relay/channel/claude/adaptor.go | 12 +-
relay/channel/claude/adaptor_test.go | 111 ++
relay/channel/claude/relay-claude.go | 103 +-
relay/channel/claude/relay_claude_test.go | 98 ++
relay/channel/claude/relay_responses.go | 4 +-
relay/channel/claude/tool_billing_test.go | 113 +-
relay/channel/cloudflare/relay_cloudflare.go | 9 +-
relay/channel/cohere/relay-cohere.go | 6 +-
relay/channel/coze/relay-coze.go | 10 +-
relay/channel/deepseek/adaptor.go | 131 +-
relay/channel/deepseek/adaptor_test.go | 270 ++++
relay/channel/deepseek/constants.go | 16 +-
relay/channel/deepseek/web_search.go | 37 +
relay/channel/dify/relay-dify.go | 22 +-
.../channel/gemini/adaptor_responses_test.go | 35 +-
relay/channel/gemini/relay-gemini-native.go | 5 +-
relay/channel/gemini/relay-gemini.go | 87 +-
.../channel/gemini/relay_gemini_usage_test.go | 205 ++-
relay/channel/gemini/relay_responses.go | 4 +-
relay/channel/jimeng/sign.go | 3 +-
relay/channel/moonshot/adaptor.go | 8 +-
relay/channel/moonshot/adaptor_test.go | 25 +
relay/channel/ollama/relay-ollama.go | 54 +-
relay/channel/ollama/stream.go | 4 +-
relay/channel/openai/adaptor.go | 3 +
relay/channel/openai/audio.go | 7 +-
relay/channel/openai/chat_via_responses.go | 96 +-
.../channel/openai/chat_via_responses_test.go | 55 +
relay/channel/openai/helper.go | 56 +-
relay/channel/openai/relay-openai.go | 14 +-
relay/channel/openai/relay_image.go | 2 +-
relay/channel/openai/relay_realtime.go | 14 +-
relay/channel/openai/relay_responses.go | 14 +-
.../openai/relay_responses_billing_test.go | 147 +-
relay/channel/openai/responses_via_chat.go | 63 +-
relay/channel/openai/web_search.go | 29 +
relay/channel/palm/relay-palm.go | 6 +-
relay/channel/task/jsplugin/adaptor.go | 516 +++++--
relay/channel/task/jsplugin/adaptor_test.go | 632 +++++++-
relay/channel/task/jsplugin/auth_test.go | 78 +
.../channel/task/jsplugin/performance_test.go | 8 +-
relay/channel/task/jsplugin/submit_stream.go | 19 +-
relay/channel/tencent/relay-tencent.go | 4 +-
relay/channel/vertex/adaptor.go | 7 +-
.../vertex/adaptor_api_key_url_test.go | 62 +
relay/channel/vertex/service_account.go | 62 +-
relay/channel/vertex/url_builder.go | 19 +-
relay/channel/vertex/url_builder_test.go | 25 +
relay/channel/xai/adaptor.go | 3 +
relay/channel/xai/text.go | 2 +-
relay/channel/xai/web_search.go | 37 +
relay/channel/xunfei/relay-xunfei.go | 10 +-
relay/channel/zhipu/relay-zhipu.go | 10 +-
relay/channel/zhipu_4v/adaptor.go | 15 +-
relay/channel/zhipu_4v/image.go | 6 +-
relay/channel/zhipu_4v/relay-zhipu_v4.go | 19 +-
relay/channel/zhipu_4v/web_search.go | 85 +
relay/common/conversion_diagnostics.go | 6 +-
relay/common/override.go | 29 +-
relay/common/override_test.go | 143 ++
relay/common/relay_info.go | 76 +-
.../common/relay_info_gemini_settings_test.go | 33 +
relay/common/relay_info_test.go | 15 +
relay/common/response_model.go | 46 +-
relay/common/response_model_guard.go | 4 +
relay/common/tool_usage.go | 125 +-
relay/common/tool_usage_test.go | 152 +-
relay/common/upstream_timing.go | 113 ++
relay/compatible_handler.go | 4 +-
relay/constant/relay_mode.go | 7 +
relay/constant/relay_mode_test.go | 2 +
relay/convert_request_error_test.go | 71 +-
relay/deepseek_tools_handler.go | 127 ++
relay/gemini_handler.go | 4 +-
relay/helper/billing_expr_request.go | 13 +-
relay/helper/common.go | 17 +-
relay/helper/group_price_test.go | 8 +-
.../image_generation_resolution_test.go | 4 +-
relay/helper/openai_image_request_test.go | 44 +-
relay/helper/price.go | 100 +-
relay/helper/price_test.go | 205 ++-
relay/helper/reasoning_suffix_test.go | 21 +
relay/helper/stream_scanner.go | 17 +-
relay/helper/valid_request.go | 31 +-
relay/image_handler.go | 48 +-
relay/mjproxy_handler.go | 35 +-
relay/plugin_protocol.go | 13 +-
relay/plugin_protocol_test.go | 8 +-
relay/relay_attempt_test.go | 3 +-
relay/relay_task.go | 30 +-
relay/relay_task_test.go | 74 +-
relay/request_billing.go | 3 +-
relay/response_model_test.go | 147 +-
relay/responses_handler.go | 2 +-
relay/responses_websocket.go | 156 +-
relay/responses_websocket_test.go | 31 +-
relay/websocket.go | 2 +-
relaykit/README.md | 163 +-
relaykit/dto/channel_settings.go | 69 +-
relaykit/dto/channel_settings_test.go | 4 +
relaykit/dto/claude.go | 95 +-
relaykit/dto/deepseek_tools_request.go | 34 +
relaykit/dto/gemini.go | 54 +-
relaykit/dto/openai_request.go | 91 +-
relaykit/dto/openai_response.go | 32 +-
relaykit/dto/token_count_meta_test.go | 100 ++
relaykit/dto/usage_merge.go | 31 +-
relaykit/dto/usage_merge_test.go | 100 +-
relaykit/dto/user_settings.go | 2 +
relaykit/relayconvert/convmeta/meta.go | 87 ++
relaykit/relayconvert/convmeta/options.go | 35 +-
relaykit/relayconvert/convmeta/websearch.go | 95 ++
.../claude_messages/to_oai_chat_req.go | 131 +-
.../claude_messages/to_oai_chat_req_test.go | 151 ++
.../claude_messages/to_oai_chat_resp.go | 10 +
.../claude_messages/to_oai_responses_req.go | 8 +-
.../internal/gemini_chat/to_oai_chat_req.go | 103 +-
relaykit/relayconvert/internal/media/media.go | 43 +-
.../oai_chat/to_claude_messages_req.go | 41 +-
.../oai_chat/to_claude_messages_resp.go | 86 +-
.../internal/oai_chat/to_gemini_chat_req.go | 9 +-
.../internal/oai_chat/to_oai_responses_req.go | 24 +-
.../oai_chat/to_oai_responses_req_test.go | 31 +
.../oai_chat/to_oai_responses_resp.go | 56 +-
.../oai_chat/to_oai_responses_resp_test.go | 410 +++++
.../oai_chat/to_oai_responses_stream_resp.go | 223 ++-
.../internal/oai_responses/req_helpers.go | 25 +
.../oai_responses/to_claude_messages_req.go | 117 +-
.../oai_responses/to_gemini_chat_req.go | 191 ++-
.../to_gemini_chat_req_preprocess.go | 101 --
.../internal/oai_responses/to_oai_chat_req.go | 219 ++-
.../oai_responses/to_oai_chat_req_test.go | 205 ++-
.../internal/shared/claude/media.go | 63 +
.../internal/shared/claude/reasoning.go | 39 +-
.../internal/shared/gemini/request.go | 65 +-
.../internal/shared/gemini/schema.go | 285 ++++
.../relayconvert/internal/toolconv/decode.go | 142 +-
.../relayconvert/internal/toolconv/encode.go | 480 +++++-
.../relayconvert/internal/toolconv/model.go | 40 +-
.../internal/toolconv/policy_test.go | 178 +++
relaykit/relayconvert/kitutil/mask.go | 186 ++-
relaykit/relayconvert/kitutil/mask_test.go | 136 ++
relaykit/relayconvert/reasoning/claude.go | 165 +-
relaykit/relayconvert/reasoning/gemini.go | 13 +-
relaykit/relayconvert/reasoning/suffix.go | 91 +-
relaykit/relayconvert/request_registry.go | 57 +-
.../relayconvert/request_registry_test.go | 371 ++++-
relaykit/relayconvert/response_registry.go | 9 +-
.../relayconvert/response_registry_test.go | 102 ++
.../openai_responses_to_openai.golden.json | 4 +-
.../openai_to_openai_responses.golden.json | 1 +
.../relayconvert/text_converter_registry.go | 4 +-
relaykit/types/endpoint_type.go | 2 +
relaykit/types/error.go | 21 +-
relaykit/types/relay_format.go | 1 +
router/access_token_scope_test.go | 202 +++
router/api-router.go | 28 +-
router/channel-router.go | 26 +-
router/channel_router_test.go | 2 +
router/main.go | 2 +-
router/plugin-router.go | 6 +-
router/plugin_router_test.go | 2 +-
router/relay-router.go | 18 +-
router/task-plugin-protocol-router.go | 8 +
router/task_plugin_protocol_router_test.go | 2 +
router/web-router.go | 13 +-
service/access_token_scope.go | 273 ++++
service/account_security.go | 22 +-
service/astra_sampling.go | 3 -
service/astra_sampling_test.go | 4 +-
service/auth_cleanup.go | 20 +-
service/auth_session.go | 19 +-
service/auth_session_test.go | 15 +-
service/auth_token_test.go | 22 +
service/authz/adapter.go | 2 +-
service/authz/enforcer.go | 2 +-
service/automatic_group.go | 21 +-
service/automatic_models_test.go | 87 +-
service/billing.go | 7 +-
service/billing_session.go | 58 +-
service/billing_usage.go | 54 +
service/candy.go | 19 -
service/channel.go | 21 +-
service/channel_affinity.go | 21 +-
service/channel_select.go | 2 +-
service/codex_channel_models.go | 18 +-
service/codex_credential_refresh_task.go | 11 +-
service/detection.go | 8 +-
service/detection_schedule.go | 13 +-
service/detection_status.go | 75 +-
service/download.go | 4 +-
service/email_binding.go | 26 +-
service/error.go | 16 +-
service/file_decoder.go | 4 +-
service/file_service.go | 2 +-
service/group.go | 5 +-
service/group_degradation.go | 52 +-
service/group_degradation_test.go | 111 +-
service/group_model_performance.go | 29 +
service/group_quality.go | 72 +-
service/group_quality_test.go | 3 +-
service/http.go | 4 +-
service/http_client.go | 7 +-
service/http_client_transport_test.go | 80 +
service/http_transport_policy.go | 11 +-
service/http_transport_sharded.go | 17 +-
service/image.go | 10 +-
service/image_billing.go | 18 +-
service/image_generation_group_test.go | 12 +-
service/log_export.go | 66 +
service/log_info_generate.go | 11 +-
service/midjourney.go | 9 +-
service/passkey/service.go | 10 +-
service/passkey/session.go | 2 +-
service/pool_failover.go | 9 +-
service/protected_fetch_client.go | 2 +-
service/quota.go | 186 ++-
service/relay_attempt.go | 9 +-
service/relay_attempt_test.go | 1 -
service/relay_error.go | 2 +-
service/relay_error_test.go | 73 +-
service/responses_usage.go | 21 +-
service/responses_usage_test.go | 33 +-
service/security_verification.go | 173 ++-
service/subscription_reset_task.go | 7 +-
service/system_instance.go | 2 +-
service/system_task.go | 22 +-
service/task_billing.go | 89 +-
service/task_billing_test.go | 88 +-
service/task_plugin_view_test.go | 25 +
service/task_polling.go | 158 +-
service/task_polling_test.go | 60 +-
service/text_quota.go | 69 +-
service/text_quota_test.go | 334 +++-
service/tiered_settle.go | 2 +-
service/tiered_settle_test.go | 5 +-
service/token_counter.go | 192 +--
service/token_estimator.go | 230 ---
service/token_routing.go | 23 +-
service/token_routing_switch_test.go | 118 +-
service/token_routing_test.go | 178 ++-
service/tokenizer.go | 63 -
service/usage_helpr.go | 3 +-
service/user_notify.go | 56 +-
service/user_notify_test.go | 191 +++
service/violation_fee.go | 6 +-
service/vision.go | 71 +
service/vision.png | Bin 0 -> 2128954 bytes
service/vision_test.go | 65 +
service/waffo_pancake.go | 6 +
service/webhook.go | 3 +-
setting/billing_setting/builtin_billing.go | 56 +
.../billing_setting/builtin_billing_test.go | 125 ++
setting/billing_setting/tiered_billing.go | 14 +-
setting/chat.go | 2 +-
setting/config/config.go | 2 +-
setting/console_setting/validation.go | 140 +-
setting/model_setting/global.go | 5 +-
setting/model_setting/qwen.go | 51 -
setting/operation_setting/log_setting.go | 45 +
setting/operation_setting/quota_setting.go | 38 +-
setting/operation_setting/tools.go | 76 +-
setting/operation_setting/tools_price_test.go | 49 +-
setting/rate_limit.go | 2 +-
setting/ratio_setting/group_ratio.go | 2 +-
setting/ratio_setting/model_ratio.go | 4 +-
setting/sensitive.go | 9 +-
setting/system_setting/task_artifact.go | 2 +-
setting/system_setting/task_artifact_store.go | 4 +-
setting/user_usable_group.go | 8 +-
tokenkit/README.md | 125 ++
tokenkit/go.mod | 15 +
tokenkit/go.sum | 14 +
tokenkit/image.go | 290 ++++
tokenkit/image_test.go | 124 ++
tokenkit/tokenizer.go | 102 ++
tokenkit/tokenkit.go | 245 +++
tokenkit/tokenkit_test.go | 83 +
web/bun.lock | 187 ++-
web/cc-switch-before.tsx | 0
web/ccswitch45-harness.tsx | 0
web/index.html | 18 +-
web/package.json | 19 +-
web/public/custom-site-icon.png | Bin 0 -> 328169 bytes
web/public/favicon.ico | Bin 15406 -> 49827 bytes
web/public/logo.png | Bin 9597 -> 13907 bytes
web/rsbuild.config.ts | 9 +
web/src/assets/brand-icons/icon-alipay.tsx | 38 +
web/src/assets/brand-icons/index.ts | 1 +
.../__tests__/command-menu.test.tsx | 130 ++
.../__tests__/date-picker-locale.test.tsx | 74 +
.../__tests__/datetime-picker-time.test.tsx | 74 +
.../__tests__/code-block-editor.test.tsx | 19 +
web/src/components/ai-elements/code-block.tsx | 3 +-
.../components/ai-elements/prompt-input.tsx | 2 +-
web/src/components/auto-group-order-item.tsx | 136 ++
web/src/components/command-menu.tsx | 25 +-
web/src/components/config-drawer.tsx | 25 +-
.../data-table/core/data-table-row.tsx | 1 +
.../data-table/layout/card-grid.tsx | 1 +
web/src/components/date-picker.tsx | 5 +-
web/src/components/datetime-picker.tsx | 27 +-
web/src/components/dialog.tsx | 4 +-
web/src/components/floating-window.tsx | 33 +-
.../__tests__/floating-window.test.tsx | 7 +-
.../components/authenticated-layout.tsx | 2 +
.../components/layout/components/footer.tsx | 9 +-
.../layout/components/section-page-layout.tsx | 2 +-
web/src/components/layout/index.ts | 5 +-
.../layout/lib/sidebar-view-registry.ts | 20 +-
web/src/components/model-group-selector.tsx | 21 +-
.../__tests__/group-detail.test.ts | 56 +
.../model-group-selector/group-detail.ts | 42 +
web/src/components/multi-select.tsx | 31 +
.../__tests__/option-icon.test.tsx | 53 +
web/src/components/permission-matrix.tsx | 238 +++
.../ui/__tests__/dialog-layout.test.tsx | 107 ++
.../components/ui/__tests__/markdown.test.tsx | 68 +
web/src/components/ui/alert-dialog.tsx | 2 +-
web/src/components/ui/combobox-input.tsx | 6 +-
web/src/components/ui/combobox.tsx | 6 +-
web/src/components/ui/dialog.tsx | 2 +-
.../__tests__/theme-boot-script.test.ts | 95 ++
.../__tests__/theme-preferences.test.tsx | 301 ++++
web/src/context/search-provider.tsx | 3 -
.../context/theme-customization-provider.tsx | 91 +-
web/src/context/theme-provider.tsx | 23 +-
web/src/features/auth/api.ts | 7 +-
.../auth/components/oauth-callback-screen.tsx | 9 +-
.../passkey/hooks/use-passkey-management.ts | 3 +-
.../__tests__/verification-flow.test.tsx | 141 ++
.../components/secure-verification-dialog.tsx | 31 +-
.../hooks/use-secure-verification.ts | 102 +-
.../auth/secure-verification/types.ts | 50 +-
.../sign-in/components/user-auth-form.tsx | 2 +-
web/src/features/auth/types.ts | 5 +
web/src/features/channels/api.ts | 20 +
.../__tests__/channel-configuration.test.tsx | 358 ++++-
...ls-columns-status-tooltip-content.test.tsx | 77 +
.../model-mapping-batch-dialog.test.tsx | 243 ++-
.../__tests__/model-mapping-editor.test.tsx | 16 +-
.../__tests__/param-override-editor.test.tsx | 355 +++++
.../__tests__/priority-guide.test.tsx | 352 +++++
.../__tests__/priority-order.test.tsx | 889 +++++++++++
.../__tests__/provider-selection.test.tsx | 8 +-
.../responses-websocket-setting.test.tsx | 52 +-
.../__tests__/table-refresh.test.tsx | 155 ++
.../upstream-model-selection.test.tsx | 24 +
.../__tests__/upstream-update-dialog.test.tsx | 363 +++++
.../channels/components/channel-card.tsx | 94 +-
.../components/channel-priority-context.ts | 29 +
.../components/channel-priority-controls.tsx | 275 ++++
.../components/channel-priority-demo.tsx | 261 ++++
.../components/channel-priority-drop-hint.tsx | 91 ++
.../components/channel-priority-guide.tsx | 166 ++
.../channels/components/channels-columns.tsx | 10 +-
.../channels/components/channels-dialogs.tsx | 10 +-
.../components/channels-primary-buttons.tsx | 117 +-
.../channels/components/channels-provider.tsx | 34 +-
.../channels/components/channels-table.tsx | 351 +++--
.../components/data-table-row-actions.tsx | 18 +-
.../dialogs/advanced-custom-editor-dialog.tsx | 50 +-
.../dialogs/channel-test-dialog.tsx | 2 +-
.../components/dialogs/codex-usage-dialog.tsx | 4 +-
.../components/dialogs/edit-tag-dialog.tsx | 2 +-
.../dialogs/multi-key-manage-dialog.tsx | 5 +-
.../dialogs/ollama-models-dialog.tsx | 7 +-
.../dialogs/param-override-editor-dialog.tsx | 106 +-
.../dialogs/upstream-update-dialog.tsx | 504 +++---
.../drawers/channel-mutate-drawer.tsx | 239 ++-
.../components/model-mapping-batch-dialog.tsx | 559 ++++---
.../components/model-mapping-editor.tsx | 5 +
.../components/model-redirect-panel.tsx | 78 +-
.../components/numeric-spinner-input.tsx | 2 +
.../components/upstream-model-selection.tsx | 4 +-
web/src/features/channels/constants.ts | 13 +-
.../hooks/use-channel-priority-drag.ts | 794 ++++++++++
.../hooks/use-channel-priority-order.ts | 251 +++
.../hooks/use-channel-upstream-updates.ts | 214 ++-
.../lib/__tests__/aws-channel.test.ts | 78 +
.../lib/__tests__/new-api-channel.test.ts | 144 ++
.../lib/__tests__/priority-preview.test.ts | 94 ++
.../lib/__tests__/vertex-channel.test.ts | 114 ++
.../features/channels/lib/advanced-custom.ts | 21 +-
.../features/channels/lib/channel-actions.ts | 9 +-
.../channels/lib/channel-configuration.ts | 2 +
.../channels/lib/channel-form-errors.ts | 1 +
web/src/features/channels/lib/channel-form.ts | 45 +-
.../channels/lib/channel-plugin-extensions.ts | 45 +-
.../channels/lib/channel-priority-order.ts | 86 ++
.../features/channels/lib/channel-utils.ts | 2 +-
web/src/features/channels/types.ts | 6 +
.../dashboard/components/flow/flow-charts.tsx | 24 +-
.../dashboard/lib/__tests__/charts.test.ts | 156 ++
web/src/features/dashboard/lib/charts.ts | 72 +-
web/src/features/dashboard/lib/flow.test.ts | 22 +
web/src/features/dashboard/lib/flow.ts | 19 +-
web/src/features/dashboard/types.ts | 19 +-
.../errors/__tests__/general-error.test.tsx | 168 ++
web/src/features/errors/general-error.tsx | 29 +-
.../group-status/__tests__/detection.test.tsx | 123 +-
.../group-status/__tests__/metrics.test.tsx | 16 +-
.../__tests__/status-page.test.tsx | 142 +-
.../features/group-status/detection-panel.tsx | 97 +-
web/src/features/group-status/index.tsx | 8 +-
.../group-status/status-page-settings.tsx | 2 +-
web/src/features/group-status/status-page.tsx | 132 +-
.../__tests__/api-addresses.test.tsx | 216 +++
.../__tests__/api-keys-mutate-drawer.test.tsx | 122 +-
.../automatic-routing-settings.test.tsx | 130 ++
.../__tests__/group-personalization.test.tsx | 5 +
.../keys/components/api-key-group-cell.tsx | 1 +
.../components/api-key-group-combobox.tsx | 7 +-
.../keys/components/api-keys-columns.tsx | 1 +
.../components/api-keys-mutate-drawer.tsx | 39 +-
.../components/api-keys-primary-buttons.tsx | 88 +-
.../components/auto-group-order-editor.tsx | 113 +-
.../components/automatic-routing-settings.tsx | 73 +-
.../__tests__/cc-switch-dialog.test.tsx | 127 ++
.../components/dialogs/cc-switch-dialog.tsx | 1 +
.../features/keys/components/group-tags.tsx | 11 +-
.../keys/components/routing-pool-editor.tsx | 41 +-
.../lib/__tests__/auto-group-form.test.ts | 49 +-
web/src/features/keys/types.ts | 13 +-
.../pricing-conversion-dialog.tsx | 2 +-
.../models/__tests__/metadata-sync.test.tsx | 193 +++
.../dialogs/missing-models-dialog.tsx | 2 +-
.../dialogs/model-delete-dialog.tsx | 7 +-
.../components/dialogs/sync-wizard-dialog.tsx | 68 +-
.../dialogs/update-config-dialog.tsx | 2 +-
.../dialogs/view-details-dialog.tsx | 2 +-
.../models/components/models-columns.tsx | 606 ++++----
.../models/components/models-table.tsx | 7 +-
web/src/features/performance-metrics/api.ts | 6 +-
web/src/features/performance-metrics/types.ts | 1 +
web/src/features/playground/api.ts | 5 +-
.../__tests__/message-streaming.test.ts | 129 ++
.../lib/message/message-streaming-utils.ts | 4 +-
.../lib/storage/__tests__/storage.test.ts | 109 ++
.../playground/lib/storage/storage.ts | 34 +-
.../playground/lib/streaming/stream-utils.ts | 5 +-
web/src/features/playground/types.ts | 8 +-
.../pricing/__tests__/dynamic-price.test.ts | 1 +
.../pricing/__tests__/group-pricing.test.ts | 104 +-
.../pricing/__tests__/model-cards.test.tsx | 395 ++++-
.../__tests__/pricing-controls.test.tsx | 84 +-
.../pricing/__tests__/sorting.test.ts | 126 ++
.../pricing/__tests__/task-expr.test.ts | 24 +-
.../__tests__/task-matrix-display.test.ts | 153 +-
.../pricing/__tests__/task-matrix.test.ts | 119 +-
.../__tests__/task-price-display.test.tsx | 119 ++
.../pricing/components/cached-price-cell.tsx | 6 +-
.../components/dynamic-pricing-breakdown.tsx | 7 +-
.../pricing/components/model-card-grid.tsx | 37 +-
.../pricing/components/model-card.tsx | 32 +-
.../pricing/components/model-details.tsx | 114 +-
.../pricing/components/model-perf-badge.tsx | 62 +-
.../pricing/components/model-price-cell.tsx | 6 +-
.../pricing/components/pricing-sidebar.tsx | 73 +-
web/src/features/pricing/constants.ts | 16 +-
.../pricing/hooks/use-billing-time.ts | 15 +-
web/src/features/pricing/lib/billing-expr.ts | 1 +
.../pricing/lib/billing-expression/README.md | 2 +-
.../pricing/lib/billing-expression/display.ts | 56 +-
.../lib/billing-expression/task-display.ts | 330 ++++
.../pricing/lib/breakdown-tier-match.ts | 25 +-
web/src/features/pricing/lib/dynamic-price.ts | 35 +-
web/src/features/pricing/lib/filters.ts | 80 +-
web/src/features/pricing/lib/price.ts | 2 +-
.../pricing/lib/task-matrix-display.ts | 35 +-
.../pricing/lib/task-price-display.ts | 45 +-
.../profile/__tests__/settings.test.tsx | 163 +-
.../components/profile-settings-card.tsx | 11 +-
.../components/tabs/notification-tab.tsx | 497 +++---
web/src/features/profile/constants.ts | 3 -
web/src/features/profile/index.tsx | 5 +
web/src/features/profile/lib/user-settings.ts | 25 +-
.../redemptions-primary-buttons.tsx | 11 +-
.../__tests__/account-security.test.tsx | 73 +
.../features/security/__tests__/page.test.tsx | 17 +-
web/src/features/security/api.ts | 125 +-
.../__tests__/access-token-card.test.tsx | 401 -----
.../access-token-create-dialog.test.tsx | 440 ++++++
.../__tests__/access-tokens-card.test.tsx | 656 ++++++++
.../__tests__/login-session-item.test.tsx | 64 +
.../security/components/access-token-card.tsx | 221 ---
.../security/components/access-token-item.tsx | 136 ++
.../components/access-tokens-card.tsx | 324 ++++
.../security/components/account-bindings.tsx | 14 +-
.../dialogs/access-token-create-dialog.tsx | 207 +++
.../dialogs/access-token-dialog.tsx | 6 +-
.../dialogs/access-token-edit-dialog.tsx | 176 +++
.../dialogs/delete-account-dialog.tsx | 8 +-
.../dialogs/two-fa-setup-dialog.tsx | 13 +-
.../components/login-session-item.tsx | 11 +-
.../security/components/passkey-card.tsx | 8 +-
.../security/hooks/use-access-token.ts | 128 --
.../security/hooks/use-access-tokens.ts | 226 +++
web/src/features/security/index.tsx | 4 +-
.../security/lib/access-token-catalog.ts | 41 +
.../security/lib/access-token-schema.ts | 120 ++
.../dialogs/user-subscriptions-dialog.tsx | 5 +-
.../components/subscriptions-columns.tsx | 6 +-
.../subscriptions-mutate-drawer.tsx | 5 +-
.../auth/__tests__/oauth-settings.test.tsx | 339 ++++
.../components/provider-form-dialog.tsx | 2 +-
.../system-settings/auth/oauth-section.tsx | 18 +-
.../system-settings/auth/passkey-section.tsx | 3 +-
.../system-settings/billing/index.tsx | 4 +-
.../billing/section-registry.tsx | 7 +-
.../content/announcements-section.tsx | 24 +-
.../__tests__/pre-consume-settings.test.tsx | 147 ++
.../general/quota-settings-section.tsx | 78 +-
.../general/system-info-section.tsx | 29 +
.../hooks/use-update-option.ts | 4 +
.../__tests__/email-notifications.test.tsx | 125 ++
.../integrations/email-settings-section.tsx | 104 +-
.../monitoring-settings-section.tsx | 47 +-
.../integrations/payment-settings-section.tsx | 5 +-
.../waffo-pancake-settings-section.tsx | 6 +-
.../__tests__/log-settings.test.tsx | 184 +++
.../maintenance/history-log-cleanup.tsx | 295 ++++
.../maintenance/log-settings-section.tsx | 672 ++------
.../maintenance/server-log-files.tsx | 292 ++++
.../__tests__/group-pricing-decimals.test.tsx | 89 ++
.../models/__tests__/group-settings.test.tsx | 352 +++++
.../model-ratio-table-selection.test.tsx | 125 ++
.../__tests__/request-simulation.test.tsx | 17 +
.../__tests__/task-pricing-copy.test.tsx | 48 +
.../__tests__/tool-price-validation.test.tsx | 35 +
.../models/channel-selector-dialog.tsx | 2 +-
.../models/claude-settings-card.tsx | 2 +-
.../models/conflict-confirm-dialog.tsx | 2 +-
.../models/gemini-settings-card.tsx | 2 +-
.../models/group-ratio-form.tsx | 208 +--
.../models/group-ratio-visual-editor.tsx | 369 +++--
.../models/group-special-usable-editor.tsx | 109 +-
.../models/group-tags-editor.tsx | 2 +-
.../models/model-ratio-visual-editor.tsx | 10 +-
.../models/tiered-pricing-editor.tsx | 12 +
.../models/tool-price-settings.tsx | 22 +-
.../models/upstream-price-cells.tsx | 25 +-
.../models/upstream-ratio-sync-columns.tsx | 9 +-
.../system-settings/operations/index.tsx | 3 +
.../operations/section-registry.tsx | 15 +-
.../features/system-settings/site/index.tsx | 1 +
.../system-settings/site/section-registry.tsx | 3 +
web/src/features/system-settings/types.ts | 8 +-
.../marketplace-install-dialog.test.tsx | 29 +-
.../__tests__/marketplace-panel.test.tsx | 37 +-
.../marketplace-plugin-logo.test.tsx | 3 +-
.../__tests__/plugin-detail-sheet.test.tsx | 15 +
.../__tests__/plugin-icon-file.test.ts | 19 +
.../__tests__/plugin-meta-preview.test.ts | 116 +-
.../task-plugins/__tests__/plugin-url.test.ts | 21 +-
.../__tests__/plugins-table.test.tsx | 39 +
.../__tests__/upload-dialog.test.tsx | 217 ++-
.../components/marketplace-capabilities.tsx | 3 +-
.../components/marketplace-install-dialog.tsx | 2 +-
.../components/marketplace-panel.tsx | 6 +-
.../components/plugin-source-picker.tsx | 2 +-
.../components/plugin-url-import-field.tsx | 2 +-
.../task-plugins/components/plugins-table.tsx | 66 +-
.../task-plugins/components/upload-dialog.tsx | 152 +-
.../task-plugins/lib/host-protocols.ts | 6 +
.../task-plugins/lib/plugin-icon-file.ts | 4 +-
.../task-plugins/lib/plugin-meta-preview.ts | 152 +-
.../features/task-plugins/lib/plugin-url.ts | 8 +-
.../usage-logs/__tests__/artifacts.test.ts | 69 +
web/src/features/usage-logs/api.ts | 58 +-
.../audit/__tests__/details.test.tsx | 264 +++-
.../audit/__tests__/viewer.test.tsx | 55 +
.../components/audit-log-details-dialog.tsx | 21 +-
.../audit/components/audit-log-filter-bar.tsx | 18 +-
.../audit/components/audit-log-viewer.tsx | 8 +-
.../usage-logs/audit/lib/audit-details.ts | 111 +-
.../billing-source-visibility.test.tsx | 196 +++
.../__tests__/cost-display.test.tsx | 102 +-
.../__tests__/detail-preview.test.tsx | 153 +-
.../__tests__/export-dialog.test.tsx | 277 ++++
.../components/__tests__/mobile-card.test.tsx | 26 +-
.../__tests__/mobile-filter.test.tsx | 64 +-
.../components/__tests__/model-badge.test.tsx | 17 +-
.../__tests__/task-artifacts-cell.test.tsx | 86 ++
.../components/__tests__/usage-facts.test.tsx | 1 -
.../components/columns/column-helpers.tsx | 8 +-
.../columns/common-logs-columns.tsx | 29 +-
.../columns/drawing-logs-columns.tsx | 314 ++--
.../components/columns/task-logs-columns.tsx | 334 ++--
.../components/common-log-mobile-card.tsx | 5 +-
.../components/common-logs-filter-bar.tsx | 33 +-
.../compact-date-time-range-picker.tsx | 14 +-
.../dialogs/audio-preview-dialog.tsx | 56 +-
.../components/dialogs/details-dialog.tsx | 22 +-
.../components/dialogs/export-logs-dialog.tsx | 195 +++
.../dialogs/task-details-dialog.tsx | 6 +-
.../components/log-cost-display.tsx | 63 +-
.../components/logs-filter-toolbar.tsx | 3 +
.../usage-logs/components/model-badge.tsx | 19 +-
.../usage-logs/components/task-artifacts.tsx | 78 +-
.../components/timing-metrics-cell.tsx | 4 +-
.../components/usage-logs-table.tsx | 35 +-
.../lib/__tests__/billing-source.test.ts | 102 ++
.../usage-logs/lib/__tests__/export.test.ts | 143 ++
.../lib/__tests__/response-model.test.ts | 90 ++
.../lib/__tests__/token-name.test.ts | 48 +
.../features/usage-logs/lib/billing-source.ts | 44 +
web/src/features/usage-logs/lib/columns.ts | 4 +-
web/src/features/usage-logs/lib/export.ts | 123 ++
web/src/features/usage-logs/lib/format.ts | 37 +-
web/src/features/usage-logs/lib/index.ts | 3 +
.../features/usage-logs/lib/response-model.ts | 54 +
.../features/usage-logs/lib/task-artifacts.ts | 44 +-
web/src/features/usage-logs/types.ts | 34 +-
web/src/features/users/api.ts | 101 +-
.../__tests__/group-filter.test.tsx | 154 ++
.../components/__tests__/permissions.test.tsx | 76 +-
.../__tests__/quota-display.test.tsx | 11 +-
.../__tests__/step-up-verification.test.tsx | 200 +++
.../components/data-table-row-actions.tsx | 76 +-
.../__tests__/user-binding-dialog.test.tsx | 64 +-
.../dialogs/user-binding-dialog.tsx | 43 +-
.../users/components/users-columns.tsx | 454 +++---
.../users/components/users-delete-dialog.tsx | 29 +-
.../users/components/users-mutate-drawer.tsx | 118 +-
.../users/components/users-provider.tsx | 58 +-
.../features/users/components/users-table.tsx | 35 +-
.../features/wallet/__tests__/copy.test.tsx | 127 ++
.../dialogs/billing-history-dialog.tsx | 47 +-
.../wallet/components/recharge-form-card.tsx | 28 +-
.../features/wallet/hooks/use-affiliate.ts | 9 +-
.../wallet/hooks/use-waffo-pancake-payment.ts | 5 +-
.../wallet/hooks/use-waffo-payment.ts | 5 +-
.../lib/__tests__/payment-icons.test.tsx | 50 +
web/src/features/wallet/lib/format.ts | 6 +-
web/src/features/wallet/lib/payment.ts | 18 +
web/src/features/wallet/lib/ui.tsx | 10 +-
web/src/hooks/use-sidebar-data.ts | 1 +
web/src/i18n/languages.test.ts | 49 +
web/src/i18n/languages.ts | 1 +
web/src/i18n/locales/en.json | 1363 +++++++++++-----
web/src/i18n/locales/fr.json | 1312 ++++++++++++----
web/src/i18n/locales/ja.json | 1310 ++++++++++++----
web/src/i18n/locales/ru.json | 1310 ++++++++++++----
web/src/i18n/locales/vi.json | 1310 ++++++++++++----
web/src/i18n/locales/zh-TW.json | 1312 ++++++++++++----
web/src/i18n/locales/zh.json | 1367 ++++++++++++-----
web/src/i18n/static-keys.ts | 97 +-
web/src/lib/__tests__/dom-utils.test.ts | 46 +
web/src/lib/__tests__/lobe-icon.test.tsx | 144 ++
web/src/lib/__tests__/model-provider.test.ts | 69 +
web/src/lib/__tests__/theme-color.test.tsx | 103 ++
web/src/lib/admin-permissions.ts | 30 +
web/src/lib/api.ts | 1 +
web/src/lib/dom-utils.ts | 7 +-
web/src/lib/frontend-cache.ts | 3 +
web/src/lib/lobe-icon.tsx | 149 +-
web/src/lib/model-provider.ts | 19 +-
web/src/lib/secure-verification.ts | 7 +-
web/src/lib/server-error-message.test.ts | 55 +-
web/src/lib/server-error-message.ts | 31 +-
web/src/lib/theme-color.ts | 71 +
web/src/lib/theme-customization.ts | 35 +-
web/src/lib/theme-storage.ts | 57 +
web/src/routes/_authenticated/users/index.tsx | 2 +-
web/src/routes/index.tsx | 4 +-
web/src/styles/index.css | 21 +-
web/src/styles/theme-presets.css | 12 +-
web/src/styles/theme.css | 15 +-
1022 files changed, 69903 insertions(+), 19207 deletions(-)
create mode 100644 .github/dependabot.yml
delete mode 100644 Dockerfile.affinity-switch
delete mode 100644 Dockerfile.browser-performance
delete mode 100644 Dockerfile.candy-test
delete mode 100644 Dockerfile.ccswitch
delete mode 100644 Dockerfile.combined
delete mode 100644 Dockerfile.compact-uptime
delete mode 100644 Dockerfile.deployed
delete mode 100644 Dockerfile.downgrade-rate
delete mode 100644 Dockerfile.fingerprint-color
delete mode 100644 Dockerfile.fingerprint-runtime
delete mode 100644 Dockerfile.group-degradation
delete mode 100644 Dockerfile.group-pricing
delete mode 100644 Dockerfile.meow
delete mode 100644 Dockerfile.modeltrace
delete mode 100644 Dockerfile.public-pelican
delete mode 100644 Dockerfile.request-status
delete mode 100644 Dockerfile.stream-health
create mode 100644 assets/screenshots/dashboard.en.jpg
create mode 100644 assets/screenshots/dashboard.zh-CN.jpg
create mode 100644 assets/screenshots/models.en.jpg
create mode 100644 assets/screenshots/models.zh-CN.jpg
create mode 100644 assets/screenshots/plugin-marketplace.en.jpg
create mode 100644 assets/screenshots/plugin-marketplace.zh-CN.jpg
create mode 100644 assets/screenshots/usage-logs.en.jpg
create mode 100644 assets/screenshots/usage-logs.zh-CN.jpg
delete mode 100644 bin/migration_v0.2-v0.3.sql
delete mode 100644 bin/migration_v0.3-v0.4.sql
delete mode 100644 bin/time_test.sh
delete mode 100644 build-fingerprint-backend.sh
create mode 100644 common/container_usage.go
create mode 100644 common/log_text.go
create mode 100644 common/log_text.zh-CN.json
create mode 100644 common/message.go
delete mode 100644 common/quota.go
create mode 100644 common/system_monitor_test.go
create mode 100644 controller/admin_user_verification_test.go
create mode 100644 controller/bot_balance.go
create mode 100644 controller/bot_balance_test.go
create mode 100644 controller/chain_detection.go
create mode 100644 controller/channel_billing_sub2api_test.go
create mode 100644 controller/channel_priority.go
create mode 100644 controller/ciallo_detection.go
create mode 100644 controller/detection_artwork.go
create mode 100644 controller/group_status_events.go
create mode 100644 controller/group_status_events_test.go
create mode 100644 controller/log_export_test.go
delete mode 100644 controller/manxue_artwork_url.go
create mode 100644 controller/model_pricing_conversion_endpoints_test.go
create mode 100644 controller/plugin_protocol_image.go
create mode 100644 controller/vision_detection.go
create mode 100644 docs/group-status-events.md
create mode 100644 i18n/i18n_test.go
create mode 100644 middleware/access_token_routes.go
create mode 100644 middleware/frontend_static.go
create mode 100644 middleware/frontend_static_test.go
create mode 100644 model/bot_balance.go
create mode 100644 model/channel_priority.go
create mode 100644 model/channel_priority_test.go
create mode 100644 model/group_model_performance.go
create mode 100644 model/group_model_performance_test.go
create mode 100644 model/user_access_token.go
create mode 100644 pkg/jsplugin/fetch.go
create mode 100644 plugins/tasks/xai/plugin.js
create mode 100644 plugins/xai_responses_test.go
delete mode 100644 relay/channel/ali/image.go
delete mode 100644 relay/channel/ali/image_wan.go
create mode 100644 relay/channel/ali/web_search.go
create mode 100644 relay/channel/aws/mantle.go
create mode 100644 relay/channel/deepseek/adaptor_test.go
create mode 100644 relay/channel/deepseek/web_search.go
create mode 100644 relay/channel/openai/web_search.go
create mode 100644 relay/channel/vertex/adaptor_api_key_url_test.go
create mode 100644 relay/channel/vertex/url_builder_test.go
create mode 100644 relay/channel/xai/web_search.go
create mode 100644 relay/channel/zhipu_4v/web_search.go
create mode 100644 relay/common/upstream_timing.go
create mode 100644 relay/deepseek_tools_handler.go
create mode 100644 relaykit/dto/deepseek_tools_request.go
create mode 100644 relaykit/dto/token_count_meta_test.go
create mode 100644 relaykit/relayconvert/convmeta/websearch.go
create mode 100644 relaykit/relayconvert/internal/claude_messages/to_oai_chat_req_test.go
delete mode 100644 relaykit/relayconvert/internal/oai_responses/to_gemini_chat_req_preprocess.go
create mode 100644 relaykit/relayconvert/internal/shared/claude/media.go
create mode 100644 relaykit/relayconvert/kitutil/mask_test.go
create mode 100644 router/access_token_scope_test.go
create mode 100644 service/access_token_scope.go
delete mode 100644 service/candy.go
create mode 100644 service/group_model_performance.go
create mode 100644 service/log_export.go
delete mode 100644 service/token_estimator.go
delete mode 100644 service/tokenizer.go
create mode 100644 service/user_notify_test.go
create mode 100644 service/vision.go
create mode 100644 service/vision.png
create mode 100644 service/vision_test.go
delete mode 100644 setting/model_setting/qwen.go
create mode 100644 setting/operation_setting/log_setting.go
create mode 100644 tokenkit/README.md
create mode 100644 tokenkit/go.mod
create mode 100644 tokenkit/go.sum
create mode 100644 tokenkit/image.go
create mode 100644 tokenkit/image_test.go
create mode 100644 tokenkit/tokenizer.go
create mode 100644 tokenkit/tokenkit.go
create mode 100644 tokenkit/tokenkit_test.go
create mode 100644 web/cc-switch-before.tsx
create mode 100644 web/ccswitch45-harness.tsx
create mode 100644 web/public/custom-site-icon.png
create mode 100644 web/src/assets/brand-icons/icon-alipay.tsx
create mode 100644 web/src/components/__tests__/command-menu.test.tsx
create mode 100644 web/src/components/__tests__/date-picker-locale.test.tsx
create mode 100644 web/src/components/__tests__/datetime-picker-time.test.tsx
create mode 100644 web/src/components/auto-group-order-item.tsx
create mode 100644 web/src/components/model-group-selector/__tests__/group-detail.test.ts
create mode 100644 web/src/components/model-group-selector/group-detail.ts
create mode 100644 web/src/components/multi-select/__tests__/option-icon.test.tsx
create mode 100644 web/src/components/permission-matrix.tsx
create mode 100644 web/src/components/ui/__tests__/dialog-layout.test.tsx
create mode 100644 web/src/components/ui/__tests__/markdown.test.tsx
create mode 100644 web/src/context/__tests__/theme-boot-script.test.ts
create mode 100644 web/src/context/__tests__/theme-preferences.test.tsx
create mode 100644 web/src/features/channels/components/__tests__/channels-columns-status-tooltip-content.test.tsx
create mode 100644 web/src/features/channels/components/__tests__/param-override-editor.test.tsx
create mode 100644 web/src/features/channels/components/__tests__/priority-guide.test.tsx
create mode 100644 web/src/features/channels/components/__tests__/priority-order.test.tsx
create mode 100644 web/src/features/channels/components/__tests__/table-refresh.test.tsx
create mode 100644 web/src/features/channels/components/__tests__/upstream-update-dialog.test.tsx
create mode 100644 web/src/features/channels/components/channel-priority-context.ts
create mode 100644 web/src/features/channels/components/channel-priority-controls.tsx
create mode 100644 web/src/features/channels/components/channel-priority-demo.tsx
create mode 100644 web/src/features/channels/components/channel-priority-drop-hint.tsx
create mode 100644 web/src/features/channels/components/channel-priority-guide.tsx
create mode 100644 web/src/features/channels/hooks/use-channel-priority-drag.ts
create mode 100644 web/src/features/channels/hooks/use-channel-priority-order.ts
create mode 100644 web/src/features/channels/lib/__tests__/aws-channel.test.ts
create mode 100644 web/src/features/channels/lib/__tests__/priority-preview.test.ts
create mode 100644 web/src/features/channels/lib/__tests__/vertex-channel.test.ts
create mode 100644 web/src/features/channels/lib/channel-priority-order.ts
create mode 100644 web/src/features/dashboard/lib/__tests__/charts.test.ts
create mode 100644 web/src/features/errors/__tests__/general-error.test.tsx
create mode 100644 web/src/features/keys/components/__tests__/api-addresses.test.tsx
create mode 100644 web/src/features/keys/components/dialogs/__tests__/cc-switch-dialog.test.tsx
create mode 100644 web/src/features/playground/lib/message/__tests__/message-streaming.test.ts
create mode 100644 web/src/features/playground/lib/storage/__tests__/storage.test.ts
create mode 100644 web/src/features/pricing/__tests__/sorting.test.ts
create mode 100644 web/src/features/pricing/lib/billing-expression/task-display.ts
delete mode 100644 web/src/features/security/components/__tests__/access-token-card.test.tsx
create mode 100644 web/src/features/security/components/__tests__/access-token-create-dialog.test.tsx
create mode 100644 web/src/features/security/components/__tests__/access-tokens-card.test.tsx
create mode 100644 web/src/features/security/components/__tests__/login-session-item.test.tsx
delete mode 100644 web/src/features/security/components/access-token-card.tsx
create mode 100644 web/src/features/security/components/access-token-item.tsx
create mode 100644 web/src/features/security/components/access-tokens-card.tsx
create mode 100644 web/src/features/security/components/dialogs/access-token-create-dialog.tsx
create mode 100644 web/src/features/security/components/dialogs/access-token-edit-dialog.tsx
delete mode 100644 web/src/features/security/hooks/use-access-token.ts
create mode 100644 web/src/features/security/hooks/use-access-tokens.ts
create mode 100644 web/src/features/security/lib/access-token-catalog.ts
create mode 100644 web/src/features/security/lib/access-token-schema.ts
create mode 100644 web/src/features/system-settings/auth/__tests__/oauth-settings.test.tsx
create mode 100644 web/src/features/system-settings/general/__tests__/pre-consume-settings.test.tsx
create mode 100644 web/src/features/system-settings/integrations/__tests__/email-notifications.test.tsx
create mode 100644 web/src/features/system-settings/maintenance/__tests__/log-settings.test.tsx
create mode 100644 web/src/features/system-settings/maintenance/history-log-cleanup.tsx
create mode 100644 web/src/features/system-settings/maintenance/server-log-files.tsx
create mode 100644 web/src/features/system-settings/models/__tests__/group-pricing-decimals.test.tsx
create mode 100644 web/src/features/system-settings/models/__tests__/group-settings.test.tsx
create mode 100644 web/src/features/system-settings/models/__tests__/model-ratio-table-selection.test.tsx
create mode 100644 web/src/features/usage-logs/components/__tests__/billing-source-visibility.test.tsx
create mode 100644 web/src/features/usage-logs/components/__tests__/export-dialog.test.tsx
create mode 100644 web/src/features/usage-logs/components/__tests__/task-artifacts-cell.test.tsx
create mode 100644 web/src/features/usage-logs/components/dialogs/export-logs-dialog.tsx
create mode 100644 web/src/features/usage-logs/lib/__tests__/billing-source.test.ts
create mode 100644 web/src/features/usage-logs/lib/__tests__/export.test.ts
create mode 100644 web/src/features/usage-logs/lib/__tests__/response-model.test.ts
create mode 100644 web/src/features/usage-logs/lib/__tests__/token-name.test.ts
create mode 100644 web/src/features/usage-logs/lib/billing-source.ts
create mode 100644 web/src/features/usage-logs/lib/export.ts
create mode 100644 web/src/features/usage-logs/lib/response-model.ts
create mode 100644 web/src/features/users/components/__tests__/group-filter.test.tsx
create mode 100644 web/src/features/users/components/__tests__/step-up-verification.test.tsx
create mode 100644 web/src/features/wallet/__tests__/copy.test.tsx
create mode 100644 web/src/features/wallet/lib/__tests__/payment-icons.test.tsx
create mode 100644 web/src/i18n/languages.test.ts
create mode 100644 web/src/lib/__tests__/dom-utils.test.ts
create mode 100644 web/src/lib/__tests__/lobe-icon.test.tsx
create mode 100644 web/src/lib/__tests__/model-provider.test.ts
create mode 100644 web/src/lib/__tests__/theme-color.test.tsx
create mode 100644 web/src/lib/theme-color.ts
create mode 100644 web/src/lib/theme-storage.ts
diff --git a/.agents/rules/billing.md b/.agents/rules/billing.md
index 5fac8de..843948c 100644
--- a/.agents/rules/billing.md
+++ b/.agents/rules/billing.md
@@ -9,7 +9,7 @@ Mandatory conventions for quota, pricing, pre-consume, settlement, refund, and u
**Billing safety invariants:** Quota/billing code MUST never produce a negative charge (a credit) from arithmetic overflow or unvalidated input. Apply defense in depth:
- Every user-controlled quantity that becomes a billing multiplier (image `n`, video `seconds`/`duration`, resolution/quality ratios, batch counts) MUST be bounded before it reaches quota calculation. Reject out-of-range values at request validation with a 400. Existing bounds: `dto.MaxImageN` for image generation count, `relaycommon.MaxTaskDurationSeconds` for task video duration, `maxTokensLimit` (`relay/helper/valid_request.go`) for `max_tokens`-family fields on every relay format (OpenAI, Claude, Gemini, Responses). Reuse these constants instead of introducing new ad hoc limits for the same concepts. When adding a new relay format or request DTO, bound its max-tokens and count fields in its validator from day one.
-- Watch for validation bypass paths: passthrough fields (e.g. `Extra["parameters"]`), task `metadata` maps, and multipart form fields can carry the same quantities around the standard DTO validation. Any adaptor that reads a multiplier from such a path must enforce the same bound (or clamp) locally.
+- Watch for validation bypass paths: passthrough fields (e.g. `Extra["parameters"]`), task `metadata` maps, and multipart form fields can carry the same quantities around the standard DTO validation. Any adaptor that reads a multiplier from such a path must enforce the same bound (or clamp) locally. For task plugins the host does this: it rejects out-of-range duration and count members at any depth of the decoded request. The one exception is a duration of -1 (the vendor picks the length) for plugins that declare `duration-auto@1`; they must reserve the model's longest duration and settle on the usage the vendor reports.
- Durations parsed from media metadata are user/upstream-controlled too: audio file headers (transcription token counting, TTS response duration) and upstream deduction numbers (e.g. Kling `FinalUnitDeduction`) can claim absurd values. Convert them with saturation before they become token counts.
- Never convert a computed quota or token count to `int` with a bare cast like `int(float64(quota) * ratio)`, `int(math.Round(...))` on unbounded input, or `int(decimal.IntPart())`. All quota rounding/conversion is centralized in `common/quota_math.go`; use those helpers: `common.QuotaFromFloat` (truncating) for float products, `common.QuotaRound` (half-away-from-zero) where rounding is intended, and `common.QuotaFromDecimal` for decimal products. `billingexpr.QuotaRound` delegates to `common.QuotaRound`. Do not reintroduce local conversion helpers or bare casts. Single-request saturation stays at the int32 boundary so batch accumulation cannot approach 64-bit wraparound; wallet/top-up conversion uses `common.WalletQuotaFromDecimalStrict` with the JavaScript-safe `common.MaxWalletQuota` boundary. Every clamp/NaN fallback is logged via `common.SysError`.
- Saturation events are also audited: each helper has a `*Checked` variant (`common.QuotaFromFloatChecked` / `QuotaRoundChecked` / `QuotaFromDecimalChecked`) that additionally returns a `*common.QuotaClamp` when clamping occurred. Billing paths that compute a charge capture that clamp onto `relayInfo.QuotaClamp` (or thread it into task settlement) and, right before writing the consume/task log, call `attachQuotaSaturation` (in `service/log_info_generate.go`) which nests the marker under the log's `other.admin_info.quota_saturation` and emits a request-correlated `logger.LogWarn`. Nesting under `admin_info` makes it admin-only for free (non-admin log views strip `admin_info`). When adding a new billing path, use the `*Checked` variant and surface the clamp the same way so the anomaly stays auditable in both the admin log UI and backend logs.
@@ -18,11 +18,11 @@ Mandatory conventions for quota, pricing, pre-consume, settlement, refund, and u
- Fields parsed into unsigned types (`*uint`) accept huge positive JSON numbers (e.g. `18446744073686646784`, a wrapped negative); a `>= 0` check is not sufficient, an upper bound is mandatory.
- Regression tests for these invariants belong with the boundary they protect (request validators, converter helpers). See `relay/helper/openai_image_request_test.go`, `relay/common/relay_utils_test.go`, and `common/quota_math_test.go` for the expected style.
-**Upstream image quantity settlement:** The billable image count may be lowered or raised from the upstream response only through `RelayInfo.UpdateImageCount`; a zero or out-of-range count leaves the reserved request quantity in place. Derive the count from image payloads, never from JSON structure:
+**Upstream image quantity settlement:** The billable image count may be lowered or raised from the upstream response only through `RelayInfo.UpdateImageCount` on the HTTP image relay, or through the `image_count` usage fact a task plugin returns on completion; a zero or out-of-range count leaves the reserved request quantity in place. Derive the count from image payloads, never from JSON structure:
- Do NOT count `data` with gjson `data.#`, `len(data)`, or any element count. Providers behind OpenAI-compatible channels return non-standard shapes: `data` as a single object, one image split into a `url` entry and a `b64_json` entry, or entries carrying only `revised_prompt`.
- An object-shaped `data` is one image when it has a non-empty `url` or `b64_json`, otherwise zero. For arrays the count is `max(entries with non-empty url, entries with non-empty b64_json)`: a standard response uses one `response_format` so this equals the entry count, a split image bills once, and payload-less entries bill nothing. The reference implementation is `openaiImageResponseCount` in `relay/channel/openai/relay_image.go`; reuse it (or the same rule) for any channel that forwards or rebuilds OpenAI-format image responses. The xAI adaptor already inherits it through `OpenaiImageHandler`.
- When an OpenAI-format JSON body is re-emitted as SSE, forward one `image_generation.completed` event per image payload using the same shape handling (object → one event, payload-less entries dropped). The client must never receive only `[DONE]` while being charged for images.
- Real SSE streams count `image_generation.completed` / `image_edit.completed` events and only trust a lower count when upstream finished the stream (`done` / `eof`); a client abort must not reduce the charge below the requested quantity.
-- Channels with their own usage field (Ali `usage.image_count`) use that value, bounded by `dto.MaxImageN`, and fall back to the converted response length. Channels that rebuild responses from typed structs count the typed slice; none of them may read `data.#`.
+- Ali (Bailian) image models are served by the `alibaba` task plugin through the `openai_image` host protocol. Its `imageUsage` reads `usage.image_count`, then `usage.output_image_count` (Qwen-Image-3.0), and otherwise counts image payloads flattened across every `choices[].message.content[]` entry and `results[]`; payload-less parts bill nothing, and a fractional, negative, oversized, or payload-inconsistent upstream count keeps the reserved request quantity. Any other channel with its own usage field applies the same order: trust the bounded upstream count first, then count payloads. Channels that rebuild responses from typed structs count the typed slice; none of them may read `data.#`.
- Regression cases for object, split, and payload-less shapes belong in `relay/channel/openai/image_stream_test.go` (fixed-price, tiered-expression, and JSON-as-SSE forwarding tables). Extend those tables instead of adding new files.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..92902fb
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: "bun"
+ directory: "/web"
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b5578df..c20540c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,6 +11,8 @@ concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+# Checkout the event commit in both jobs. A PR merge ref can disappear while
+# queued runs start, and a base branch can advance after the merge event.
jobs:
backend:
name: Backend vet, build, and test
@@ -25,7 +27,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: ${{ github.event.pull_request.base.repo.full_name }}
- ref: ${{ github.event.action == 'closed' && github.event.pull_request.base.ref || github.ref }}
+ ref: ${{ github.sha }}
- name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
@@ -34,6 +36,7 @@ jobs:
cache-dependency-path: |
go.sum
relaykit/go.sum
+ tokenkit/go.sum
- name: Create frontend embed placeholder
run: |
@@ -47,6 +50,10 @@ jobs:
working-directory: relaykit
run: go vet ./...
+ - name: Vet tokenkit independently
+ working-directory: tokenkit
+ run: go vet ./...
+
- name: Build root module
run: go build ./...
@@ -54,7 +61,11 @@ jobs:
working-directory: relaykit
run: go build ./...
- - name: Test root and relaykit modules
+ - name: Build tokenkit independently
+ working-directory: tokenkit
+ run: go build ./...
+
+ - name: Test root, relaykit, and tokenkit modules
run: make test
frontend:
@@ -71,7 +82,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: ${{ github.event.pull_request.base.repo.full_name }}
- ref: ${{ github.event.action == 'closed' && github.event.pull_request.base.ref || github.ref }}
+ ref: ${{ github.sha }}
- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
diff --git a/.github/workflows/electron-build.yml b/.github/workflows/electron-build.yml
index 08c57c9..fa0587a 100644
--- a/.github/workflows/electron-build.yml
+++ b/.github/workflows/electron-build.yml
@@ -39,7 +39,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
- go-version: '>=1.25.1'
+ go-version: '>=1.26'
- name: Build frontend
env:
@@ -55,13 +55,13 @@ jobs:
# if: runner.os != 'Windows'
# run: |
# go mod download
- # go build -ldflags "-s -w -X 'new-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o new-api
+ # go build -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o new-api
- name: Build Go binary (Windows)
if: runner.os == 'Windows'
run: |
go mod download
- go build -ldflags "-s -w -X 'new-api/common.Version=$(git describe --tags)'" -o new-api.exe
+ go build -pgo="$(go list -m -f '{{.Dir}}' github.com/Calcium-Ion/moejs)/default.pgo" -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$(git describe --tags)'" -o new-api.exe
- name: Update Electron version
run: |
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b4bfed3..c942010 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -44,16 +44,16 @@ jobs:
- name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
- go-version: '>=1.25.1'
+ go-version: '>=1.26'
- name: Build Backend (amd64)
run: |
go mod download
- go build -ldflags "-s -w -X 'new-api/common.Version=$VERSION' -extldflags '-static'" -o new-api-$VERSION
+ go build -pgo="$(go list -m -f '{{.Dir}}' github.com/Calcium-Ion/moejs)/default.pgo" -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$VERSION' -extldflags '-static'" -o new-api-$VERSION
- name: Build Backend (arm64)
run: |
sudo apt-get update
DEBIAN_FRONTEND=noninteractive sudo apt-get install -y gcc-aarch64-linux-gnu
- CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X 'new-api/common.Version=$VERSION' -extldflags '-static'" -o new-api-arm64-$VERSION
+ CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -pgo="$(go list -m -f '{{.Dir}}' github.com/Calcium-Ion/moejs)/default.pgo" -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$VERSION' -extldflags '-static'" -o new-api-arm64-$VERSION
- name: Generate checksums
run: sha256sum new-api-* > checksums-linux.txt
@@ -98,11 +98,11 @@ jobs:
- name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
- go-version: '>=1.25.1'
+ go-version: '>=1.26'
- name: Build Backend
run: |
go mod download
- go build -ldflags "-X 'new-api/common.Version=$VERSION'" -o new-api-macos-$VERSION
+ go build -pgo="$(go list -m -f '{{.Dir}}' github.com/Calcium-Ion/moejs)/default.pgo" -ldflags "-X 'github.com/QuantumNous/new-api/common.Version=$VERSION'" -o new-api-macos-$VERSION
- name: Generate checksums
run: shasum -a 256 new-api-macos-* > checksums-macos.txt
@@ -149,11 +149,11 @@ jobs:
- name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
- go-version: '>=1.25.1'
+ go-version: '>=1.26'
- name: Build Backend
run: |
go mod download
- go build -ldflags "-s -w -X 'new-api/common.Version=$VERSION'" -o new-api-$VERSION.exe
+ go build -pgo="$(go list -m -f '{{.Dir}}' github.com/Calcium-Ion/moejs)/default.pgo" -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$VERSION'" -o new-api-$VERSION.exe
- name: Generate checksums
run: sha256sum new-api-*.exe > checksums-windows.txt
diff --git a/AGENTS.md b/AGENTS.md
index 7d68eb9..d9abbf4 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -8,18 +8,19 @@ This is an AI API gateway/proxy built with Go. It aggregates 40+ upstream AI pro
## Tech Stack
-- **Backend**: Go 1.25.1 (see each module’s `go.mod`), Gin web framework, GORM v2 ORM
+- **Backend**: Go 1.26 for the root module and `tokenkit/`, Go 1.25.1 for `relaykit/` (see each module’s `go.mod`), Gin web framework, GORM v2 ORM
- **Frontend**: React 19, TypeScript, Rsbuild 2, TanStack Router/Query/Table, Zustand, Base UI, Tailwind CSS 4
- **Databases**: SQLite, MySQL, PostgreSQL for the primary database (all three must be supported); a separately configured log database also supports ClickHouse
- **Cache**: Redis (go-redis) + in-memory cache
- **Auth**: Browser sessions, API tokens and personal access tokens, JWT, WebAuthn/Passkeys, TOTP, OAuth/OIDC; Casbin authorization in `service/authz/`
-- **Extensions**: JavaScript task plugins executed by Sobek; Electron desktop wrapper
+- **Extensions**: JavaScript task plugins executed by moejs; Electron desktop wrapper
- **Frontend package manager**: Bun (preferred over npm/yarn/pnpm)
## Architecture
- The Go gateway handles management APIs, upstream relay, billing, and background tasks across `router/`, `middleware/`, `controller/`, `service/`, `model/`, and `relay/`.
- `relaykit/` is an independent Go module for protocol DTOs and conversions; transport, authentication, database access, and billing stay in the host.
+- `tokenkit/` is an independent Go module for text and image token counting and estimation; request parsing, file loading, and billing stay in the host.
- JavaScript task plugins live in `plugins/tasks/`, run through `pkg/jsplugin/`, and integrate with host task polling and settlement.
- `web/` is the React frontend (see `web/AGENTS.md`); `electron/` is the desktop wrapper.
@@ -27,7 +28,11 @@ This is an AI API gateway/proxy built with Go. It aggregates 40+ upstream AI pro
### Backend (`i18n/`)
- Library: `nicksnyder/go-i18n/v2`
-- Languages: en, zh
+- Languages: en, zh-CN, zh-TW
+- Scope: text with no web console in between (errors returned to AI clients, emails, notifications), translated with `i18n.T` / `i18n.Translate`
+- Web console API messages are English source text built with `common.NewMessage` / `common.ApiErrorT` / `common.ApiSuccessT`; the frontend translates them, so add each key to all seven frontend locale files
+- Stored log content is `[]*common.Message`; a fail reason written by the gateway is a fixed English sentence that is also a key in the frontend locale files
+- Server log lines are English text passed through `common.LogText`; `DEFAULT_LANGUAGE=zh-CN` prints the Chinese text of the lines that have one in `common/log_text.zh-CN.json`, and is the language of backend messages for a reader who states none
### Frontend (`web/src/i18n/`)
- Library: `i18next` + `react-i18next` + `i18next-browser-languagedetector`
@@ -72,10 +77,10 @@ This is an AI API gateway/proxy built with Go. It aggregates 40+ upstream AI pro
- Remove ineffective `omitempty` tags on non-pointer struct fields only after confirming the active JSON encoder preserves the same output. Do not change field types or omission behavior as part of a style cleanup; optional relay scalar fields must still follow the pointer rules below.
- Format modified Go files with `gofmt` and remove unused imports after these changes.
-**relaykit module independence:** The `relaykit/` Go module MUST remain independently buildable.
+**relaykit and tokenkit module independence:** The `relaykit/` and `tokenkit/` Go modules MUST remain independently buildable.
-- Code under `relaykit/` MUST NOT import or depend on packages from the root `new-api` module, or rely on root-only configuration, generated files, or workspace wiring.
-- Any change affecting `relaykit/` or its public APIs MUST be verified with `cd relaykit && GOWORK=off go build ./...`; a successful root-module build is not sufficient.
+- Code under `relaykit/` or `tokenkit/` MUST NOT import or depend on packages from the root `new-api` module, or rely on root-only configuration, generated files, or workspace wiring. `tokenkit/` also MUST NOT depend on `relaykit/`.
+- Any change affecting `relaykit/`, `tokenkit/`, or their public APIs MUST be verified with `GOWORK=off go build ./...` inside that module; a successful root-module build is not sufficient.
**JSON package:** In the root Go module, all JSON marshal/unmarshal operations MUST use the wrapper functions in `common/json.go`:
diff --git a/CUSTOMIZATION.md b/CUSTOMIZATION.md
index 88b6816..8e89ec7 100644
--- a/CUSTOMIZATION.md
+++ b/CUSTOMIZATION.md
@@ -1,6 +1,6 @@
# 自定义版本说明
-当前版本为 v1.0.0-rc.38-custom.67-20261004,基于官方 v1.0.0-rc.38,对应 2026-10-04 已部署的自定义源码。
+当前版本为 v1.0.0-rc.45-custom.97,基于官方 v1.0.0-rc.45。
## 主要定制
@@ -12,16 +12,22 @@
- 主站 /group-status 使用真实分组名并展示全部分组,检测请求使用真实分组标识;独立 uptime 页应用公开展示配置。
- 渠道每 15 分钟探活,展示最近 18 次记录;正常用户请求可恢复分组正常状态。
- 两页共用分组行布局,正常与性能下降分组共同优先,同类状态中支持 Astra 的分组排在前面,其他分组使用紧凑布局;鹈鹕样例按时间倒序横向滚动,最新在左,点击缩略图预览。
-- 鹈鹕检测通过 manxue.ai 访客接口请求公开作品,保存可跳转的作品链接;连接失败时本地生成并标记为不确定。
- 作品脚本在隔离 iframe 中运行,以保留腿部、踏板等动画绘制;缩略图按容器宽度等比缩放。
+## custom.97
+
+- 视觉检测区分能力残缺、疑似降智和运行失败,保留各项检测的独立有效结论。
+- 按分组与模型展示吞吐量、首字时间:各自优先使用最近 30 次有效调用,缺少调用时使用测试数据。
+- 自动分组新增高吞吐优先策略;网站图标可通过构建变量配置。
+- Astra 优先分组预设从当前分组池选择,不绑定特定供应商。
+
## 从源码构建
- docker build -f Dockerfile.local-release -t local/new-api:v1.0.0-rc.38-custom.67-20261004 .
+ docker build -f Dockerfile.local-release -t local/new-api:v1.0.0-rc.45-custom.97 .
此入口从 Bun、Go 和 Debian 基础镜像构建前端与服务,不要求预先存在旧版 local/new-api 镜像。VERSION 用于编译版本号;Compose 如果设置了 VERSION 环境变量,也应同步更新。
-其他 Dockerfile.* 和 build-fingerprint-backend.sh 保留为历史构建参考。部分增量构建文件依赖服务器上的旧版镜像或已编译二进制,新环境请使用 Dockerfile.local-release。
+构建需要 Bun 和 Go 1.26;Dockerfile.local-release 包含完整的前端与后端构建过程。
## 运行与升级
@@ -29,7 +35,6 @@
升级前备份数据库及 Compose 配置,部署后检查容器健康、版本号、主站分组状态和公开 uptime 页面。Uptime Kuma 的停用与域名反向代理属于部署配置,不会仅因替换应用镜像自动完成。
-公开作品链接受 manxue.ai 的保留期限约束;没有公开链接的历史作品继续使用本站预览。
## custom.44 current availability
diff --git a/Dockerfile b/Dockerfile
index 32acfe7..81986bf 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -7,7 +7,7 @@ COPY ./web ./
COPY ./VERSION /build/VERSION
RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat /build/VERSION) bun run build
-FROM golang:1.26.1-alpine@sha256:2389ebfa5b7f43eeafbd6be0c3700cc46690ef842ad962f6c5bd6be49ed82039 AS builder2
+FROM golang:1.26.10-alpine@sha256:9ce231b28998b32e116927c61aa5c406183061d4f3f2d61b292c88e7f6bdf7ca AS builder2
ENV GO111MODULE=on CGO_ENABLED=0 GOWORK=off
ARG TARGETOS
@@ -18,9 +18,10 @@ ENV GOEXPERIMENT=greenteagc
WORKDIR /build
ADD go.mod go.sum ./
-# relaykit is a local submodule referenced via replace; its go.mod must be
-# present for go mod download to resolve the main module graph.
+# relaykit and tokenkit are local submodules referenced via replace; their
+# go.mod files must be present for go mod download to resolve the main module graph.
ADD relaykit/go.mod ./relaykit/go.mod
+ADD tokenkit/go.mod ./tokenkit/go.mod
RUN go mod download
COPY . .
@@ -28,7 +29,7 @@ COPY --from=builder /build/web/dist ./web/dist
RUN go test ./service -run TestAppendStreamStatusTreatsClientGoneAsDisplayNormal
RUN go test ./controller -run 'TestClientFacing(OpenAIErrorHidesUpstreamDetails|ClaudeErrorHidesUpstreamDetails|ErrorKeepsLocalValidationMessage)'
RUN go test ./controller -run TestPublicUpstreamErrorContent
-RUN go build -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$(cat VERSION)'" -o new-api
+RUN go build -pgo="$(go list -m -f '{{.Dir}}' github.com/Calcium-Ion/moejs)/default.pgo" -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$(cat VERSION)'" -o new-api
FROM debian:bookworm-slim@sha256:f06537653ac770703bc45b4b113475bd402f451e85223f0f2837acbf89ab020a
diff --git a/Dockerfile.affinity-switch b/Dockerfile.affinity-switch
deleted file mode 100644
index 2e12b92..0000000
--- a/Dockerfile.affinity-switch
+++ /dev/null
@@ -1,3 +0,0 @@
-FROM local/new-api:v1.0.0-rc.38-custom.45-20260926
-COPY new-api /new-api
-LABEL org.opencontainers.image.version="v1.0.0-rc.38-custom.46-20260927"
diff --git a/Dockerfile.browser-performance b/Dockerfile.browser-performance
deleted file mode 100644
index f8b6166..0000000
--- a/Dockerfile.browser-performance
+++ /dev/null
@@ -1,4 +0,0 @@
-FROM local/new-api:v1.0.0-rc.38-custom.59-20260929
-COPY new-api /new-api
-LABEL org.opencontainers.image.version="v1.0.0-rc.38-custom.60-20260929"
-LABEL custom.patch="browser-performance"
diff --git a/Dockerfile.candy-test b/Dockerfile.candy-test
deleted file mode 100644
index 992f78e..0000000
--- a/Dockerfile.candy-test
+++ /dev/null
@@ -1,3 +0,0 @@
-FROM local/new-api:v1.0.0-rc.38-custom.66-20261004
-COPY new-api /new-api
-LABEL org.opencontainers.image.version="v1.0.0-rc.38-custom.67"
diff --git a/Dockerfile.ccswitch b/Dockerfile.ccswitch
deleted file mode 100644
index 9d2bb83..0000000
--- a/Dockerfile.ccswitch
+++ /dev/null
@@ -1,3 +0,0 @@
-FROM local/new-api:v1.0.0-rc.38-custom.44-20260926
-COPY new-api /new-api
-LABEL org.opencontainers.image.version="v1.0.0-rc.38-custom.45-20260926"
diff --git a/Dockerfile.combined b/Dockerfile.combined
deleted file mode 100644
index 0cc08c9..0000000
--- a/Dockerfile.combined
+++ /dev/null
@@ -1,3 +0,0 @@
-FROM local/new-api:v1.0.0-rc.38-custom.51-20260928
-COPY new-api /new-api
-LABEL custom.patch="automatic-routing-pool-failover-model-costs"
diff --git a/Dockerfile.compact-uptime b/Dockerfile.compact-uptime
deleted file mode 100644
index 1ea2e1f..0000000
--- a/Dockerfile.compact-uptime
+++ /dev/null
@@ -1,3 +0,0 @@
-FROM local/new-api:v1.0.0-rc.38-custom.43-20260926
-COPY new-api /new-api
-LABEL org.opencontainers.image.version="v1.0.0-rc.38-custom.44-20260926"
diff --git a/Dockerfile.deployed b/Dockerfile.deployed
deleted file mode 100644
index dac0392..0000000
--- a/Dockerfile.deployed
+++ /dev/null
@@ -1,3 +0,0 @@
-FROM local/new-api:v1.0.0-rc.38-custom.24-20260923
-COPY new-api /new-api
-LABEL org.opencontainers.image.version="v1.0.0-rc.38-custom.25-20260923"
diff --git a/Dockerfile.dev b/Dockerfile.dev
index 47475e3..9f2419e 100644
--- a/Dockerfile.dev
+++ b/Dockerfile.dev
@@ -1,7 +1,7 @@
# Backend-only build for frontend development
# Skips frontend build and uses a placeholder for //go:embed web/dist
-FROM golang:1.26.1-alpine AS builder
+FROM golang:1.26.10-alpine@sha256:9ce231b28998b32e116927c61aa5c406183061d4f3f2d61b292c88e7f6bdf7ca AS builder
ENV GO111MODULE=on CGO_ENABLED=0
ARG TARGETOS
@@ -12,9 +12,10 @@ ENV GOEXPERIMENT=greenteagc
WORKDIR /build
ADD go.mod go.sum ./
-# relaykit is a local submodule referenced via replace; its go.mod must be
-# present for go mod download to resolve the main module graph.
+# relaykit and tokenkit are local submodules referenced via replace; their
+# go.mod files must be present for go mod download to resolve the main module graph.
ADD relaykit/go.mod ./relaykit/go.mod
+ADD tokenkit/go.mod ./tokenkit/go.mod
RUN go mod download
COPY . .
diff --git a/Dockerfile.downgrade-rate b/Dockerfile.downgrade-rate
deleted file mode 100644
index 1e380c5..0000000
--- a/Dockerfile.downgrade-rate
+++ /dev/null
@@ -1,3 +0,0 @@
-FROM local/new-api:v1.0.0-rc.38-custom.50-20260927
-COPY new-api /new-api
-LABEL org.opencontainers.image.version="v1.0.0-rc.38-custom.51-20260928"
diff --git a/Dockerfile.fingerprint-color b/Dockerfile.fingerprint-color
deleted file mode 100644
index 068c5d1..0000000
--- a/Dockerfile.fingerprint-color
+++ /dev/null
@@ -1,3 +0,0 @@
-FROM local/new-api:v1.0.0-rc.38-custom.65-20261004
-COPY new-api /new-api
-LABEL org.opencontainers.image.version="v1.0.0-rc.38-custom.66"
diff --git a/Dockerfile.fingerprint-runtime b/Dockerfile.fingerprint-runtime
deleted file mode 100644
index 151c21c..0000000
--- a/Dockerfile.fingerprint-runtime
+++ /dev/null
@@ -1,4 +0,0 @@
-FROM local/new-api:v1.0.0-rc.38-custom.26-20260923
-COPY new-api /new-api
-COPY LICENSE NOTICE THIRD-PARTY-LICENSES.md /licenses/
-COPY pkg/model_fingerprint/LICENSE-*.txt /licenses/model-fingerprint/
diff --git a/Dockerfile.group-degradation b/Dockerfile.group-degradation
deleted file mode 100644
index 117bbb6..0000000
--- a/Dockerfile.group-degradation
+++ /dev/null
@@ -1,3 +0,0 @@
-FROM local/new-api:v1.0.0-rc.38-custom.47-20260927
-COPY new-api /new-api
-LABEL org.opencontainers.image.version="v1.0.0-rc.38-custom.48-20260927"
diff --git a/Dockerfile.group-pricing b/Dockerfile.group-pricing
deleted file mode 100644
index bee6055..0000000
--- a/Dockerfile.group-pricing
+++ /dev/null
@@ -1,24 +0,0 @@
-FROM oven/bun:1.4.0 AS webbuild
-WORKDIR /build/web
-COPY web/package.json web/bun.lock ./
-RUN bun install --frozen-lockfile
-COPY web ./
-COPY VERSION /build/VERSION
-RUN bun run typecheck
-RUN bun run test src/features/pricing/__tests__/group-pricing.test.ts src/features/pricing/__tests__/dynamic-price.test.ts
-RUN bunx oxlint -c .oxlintrc.json src/features/pricing/types.ts src/features/pricing/lib/dynamic-price.ts src/features/pricing/components/model-card.tsx src/features/pricing/components/model-details.tsx src/features/pricing/__tests__/group-pricing.test.ts
-RUN VITE_REACT_APP_VERSION=$(cat /build/VERSION) bun run build
-
-FROM local/new-api:group-cache-status-tests-20260905 AS build
-ENV CGO_ENABLED=0 GOWORK=off GOEXPERIMENT=greenteagc
-WORKDIR /build
-COPY go.mod go.sum ./
-COPY relaykit/go.mod ./relaykit/go.mod
-RUN go mod download
-COPY . .
-COPY --from=webbuild /build/web/dist ./web/dist
-RUN go test ./relay/helper ./setting/billing_setting ./controller ./service -run 'TestGroupModelPricing|TestModelPriceHelperTiered|TestAppendStreamStatusTreatsClientGoneAsDisplayNormal|TestClientFacing|TestPublicUpstreamErrorContent'
-RUN go build -ldflags "-s -w -X github.com/QuantumNous/new-api/common.Version=$(cat VERSION)" -o /new-api
-
-FROM local/new-api:v1.0.0-rc.30-group-cache-status-20260906-r4
-COPY --from=build /new-api /new-api
diff --git a/Dockerfile.local-release b/Dockerfile.local-release
index 773dc0a..c3dd521 100644
--- a/Dockerfile.local-release
+++ b/Dockerfile.local-release
@@ -3,9 +3,9 @@ WORKDIR /build/web
COPY web/package.json web/bun.lock ./
RUN bun install --frozen-lockfile
COPY web/ ./
-RUN bun run build
+RUN VITE_REACT_APP_FAVICON=/custom-site-icon.png bun run build
-FROM golang:1.25.1 AS build
+FROM golang:1.26.10@sha256:672c944cfd07599b5f1bb179869270cdc94c6254e41db4d431259693cb286b82 AS build
ENV GOMAXPROCS=2 GOFLAGS=-p=2 GO111MODULE=on CGO_ENABLED=0 GOWORK=off
ARG TARGETOS
ARG TARGETARCH
@@ -25,7 +25,7 @@ RUN apt-get update \
COPY --from=build /build/new-api /
COPY LICENSE NOTICE THIRD-PARTY-LICENSES.md /licenses/
COPY pkg/model_fingerprint/LICENSE-*.txt /licenses/model-fingerprint/
-LABEL org.opencontainers.image.version="v1.0.0-rc.38-custom.67-20261004"
+LABEL org.opencontainers.image.version="v1.0.0-rc.45-custom.97"
EXPOSE 3000
WORKDIR /data
ENTRYPOINT ["/new-api"]
diff --git a/Dockerfile.meow b/Dockerfile.meow
deleted file mode 100644
index 8d1c5d7..0000000
--- a/Dockerfile.meow
+++ /dev/null
@@ -1,9 +0,0 @@
-FROM debian:bookworm-slim@sha256:f06537653ac770703bc45b4b113475bd402f451e85223f0f2837acbf89ab020a
-RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates tzdata libasan8 wget && rm -rf /var/lib/apt/lists/* && update-ca-certificates
-COPY new-api /new-api
-COPY LICENSE NOTICE THIRD-PARTY-LICENSES.md /licenses/
-COPY pkg/model_fingerprint/LICENSE-*.txt /licenses/model-fingerprint/
-LABEL org.opencontainers.image.version="v1.0.0-rc.38-custom.64"
-EXPOSE 3000
-WORKDIR /data
-ENTRYPOINT ["/new-api"]
diff --git a/Dockerfile.modeltrace b/Dockerfile.modeltrace
deleted file mode 100644
index b24300c..0000000
--- a/Dockerfile.modeltrace
+++ /dev/null
@@ -1,10 +0,0 @@
-FROM debian:bookworm-slim@sha256:f06537653ac770703bc45b4b113475bd402f451e85223f0f2837acbf89ab020a
-RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates tzdata libasan8 wget && rm -rf /var/lib/apt/lists/* && update-ca-certificates
-COPY new-api /new-api
-COPY LICENSE NOTICE THIRD-PARTY-LICENSES.md /licenses/
-COPY pkg/model_fingerprint/LICENSE-*.txt /licenses/model-fingerprint/
-COPY pkg/model_benchmark/LICENSE-*.txt /licenses/model-benchmark/
-LABEL org.opencontainers.image.version="v1.0.0-rc.38-custom.65"
-EXPOSE 3000
-WORKDIR /data
-ENTRYPOINT ["/new-api"]
diff --git a/Dockerfile.public-pelican b/Dockerfile.public-pelican
deleted file mode 100644
index 26fafe5..0000000
--- a/Dockerfile.public-pelican
+++ /dev/null
@@ -1,3 +0,0 @@
-FROM local/new-api:v1.0.0-rc.38-custom.38-20260926
-COPY new-api /new-api
-LABEL org.opencontainers.image.version="v1.0.0-rc.38-custom.39-20260926"
diff --git a/Dockerfile.request-status b/Dockerfile.request-status
deleted file mode 100644
index 989cc50..0000000
--- a/Dockerfile.request-status
+++ /dev/null
@@ -1,47 +0,0 @@
-FROM oven/bun:1.4.0@sha256:5ff609364c049b54eb0ff560ec96319729a972078ef2c755d758f0c6ef89c2d6 AS webdeps
-WORKDIR /build/web
-COPY web/package.json web/bun.lock ./
-RUN bun install --frozen-lockfile
-
-FROM node:24-alpine@sha256:50c8e8ca1d27439048670df5883f32d57cf81cff6233222c893fd0d9884cbd81 AS webbuild
-WORKDIR /build/web
-COPY --from=webdeps /build/web/node_modules ./node_modules
-COPY web ./
-COPY VERSION /build/VERSION
-COPY pkg/billingexpr/testdata /build/pkg/billingexpr/testdata
-RUN npm run typecheck
-RUN node node_modules/vitest/vitest.mjs run src/features/usage-logs/components/__tests__ src/features/system-settings/request-policies src/features/group-status/__tests__ src/features/pricing/__tests__/group-pricing.test.ts src/features/keys/lib/__tests__/auto-group-form.test.ts src/features/keys/components/__tests__/api-keys-mutate-drawer.test.tsx src/features/keys/components/__tests__/astra-cost-notice.test.tsx src/features/channels/components/__tests__/responses-websocket-setting.test.tsx src/features/profile/__tests__/settings.test.tsx
-RUN npx --no-install oxlint -c .oxlintrc.json src/features/group-status src/features/dashboard/types.ts src/features/keys/components/astra-cost-notice.tsx src/features/keys/components/__tests__/astra-cost-notice.test.tsx src/features/keys/components/api-keys-mutate-drawer.tsx src/features/keys/lib/api-key-form.ts src/features/keys/types.ts src/features/channels/components/astra-model-guard-setting.tsx src/features/channels/components/drawers/channel-mutate-drawer.tsx src/features/channels/components/__tests__/responses-websocket-setting.test.tsx src/features/channels/lib/channel-form.ts src/features/channels/lib/channel-configuration.ts src/features/profile/lib/user-settings.ts src/features/profile/types.ts src/features/usage-logs/components/routing-attempts.tsx src/features/usage-logs/components/model-badge.tsx src/features/usage-logs/components/common-logs-filter-bar.tsx
-RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat /build/VERSION) npm run build
-
-FROM golang:1.26.1-alpine@sha256:2389ebfa5b7f43eeafbd6be0c3700cc46690ef842ad962f6c5bd6be49ed82039 AS build
-ENV GOMAXPROCS=2 GOFLAGS=-p=2 GO111MODULE=on CGO_ENABLED=0 GOWORK=off GOEXPERIMENT=greenteagc
-ARG TARGETOS
-ARG TARGETARCH
-ENV GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64}
-WORKDIR /build
-ADD go.mod go.sum ./
-ADD relaykit/go.mod ./relaykit/go.mod
-RUN go mod download
-COPY . .
-COPY --from=webbuild /build/web/dist ./web/dist
-RUN go test ./pkg/perf_metrics ./model ./controller ./service ./relay/helper -run 'TestGroupBuckets|TestRequestStatus|TestGroupModelPricing|TestGetEnabledGroupSetRequiresEnabledAbilityAndChannel|TestSelectableGroupsRequireEnabledChannel|TestAddTokenPersistsFixedBackupGroup|TestUpdateTokenAutoGroupsTriStateAndFixedBackup|TestAddTokenRejectsInvalidFixedBackupGroup|TestCacheGetRandomSatisfiedChannelUsesTokenAutoGroupsWhenGlobalAutoIsEmpty|TestCacheGetRandomSatisfiedChannelUsesFixedTokenBackupGroup|TestCacheGetRandomSatisfiedChannelSkipsUnavailableFixedPrimary|TestAppendStreamStatusTreatsClientGoneAsDisplayNormal|TestClientFacingOpenAIErrorHidesUpstreamDetails|TestClientFacingClaudeErrorHidesUpstreamDetails|TestClientFacingErrorKeepsLocalValidationMessage|TestPublicUpstreamErrorContent' -count=1
-RUN go test ./model ./controller ./service -run 'TestChannelGetSettingDefaultsAstraGuardOn|TestFixedBackup|TestShouldSkipRetryAfterChannelAffinityFailure|TestShouldRetryHonorsPinRetryMode|TestGroupPool|TestBuildGroupPoolTiers|TestNextTierIndex|TestGroupAvailability|TestGetGroupPool|TestSetGroupPool' -count=1
-RUN go test ./model ./service ./controller ./relay/helper -run 'TestRequestDetails|TestSelectionTrace|TestResponsesInterruptedStreamHealth|TestResponsesWebSocketReusesConnectionAndSettlesEachRequest|TestStreamScanner' -count=1
-RUN cd relaykit && GOWORK=off go build ./...
-RUN go test ./relay -run 'TestResponseGuard|TestResponseModel' -count=1
-RUN go test ./controller ./middleware ./service -run 'TestTokenAstraMigration|TestRelayAttemptGuardResult|TestRelayAttemptSwitchSafety|TestTokenAuth|TestAstra' -count=1
-RUN go test ./pkg/model_fingerprint ./controller ./service ./router -run 'TestFingerprint|TestAstraSampling|TestSystemTask|TestChannelPermission' -count=1
-RUN go build -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$(cat VERSION)'" -o new-api
-
-FROM debian:bookworm-slim@sha256:f06537653ac770703bc45b4b113475bd402f451e85223f0f2837acbf89ab020a
-RUN apt-get update \
- && apt-get install -y --no-install-recommends ca-certificates tzdata libasan8 wget \
- && rm -rf /var/lib/apt/lists/* \
- && update-ca-certificates
-COPY --from=build /build/new-api /
-COPY LICENSE NOTICE THIRD-PARTY-LICENSES.md /licenses/
-COPY pkg/model_fingerprint/LICENSE-*.txt /licenses/model-fingerprint/
-EXPOSE 3000
-WORKDIR /data
-ENTRYPOINT ["/new-api"]
diff --git a/Dockerfile.stream-health b/Dockerfile.stream-health
deleted file mode 100644
index b678637..0000000
--- a/Dockerfile.stream-health
+++ /dev/null
@@ -1,3 +0,0 @@
-FROM local/new-api:v1.0.0-rc.38-custom.49-20260927
-COPY new-api /new-api
-LABEL org.opencontainers.image.version="v1.0.0-rc.38-custom.50-20260927"
diff --git a/README.en.md b/README.en.md
index cd0d551..58b5815 100644
--- a/README.en.md
+++ b/README.en.md
@@ -4,12 +4,13 @@
# New API
-🍥 **Next-Generation Large Model Gateway and AI Asset Management System**
+**An AI gateway for models, applications, and agents**
- 中文 |
- English |
- Français |
+ 简体中文 |
+ 繁體中文 |
+ English |
+ Français |
日本語
@@ -45,26 +46,54 @@
- Quick Start •
- Key Features •
- Deployment •
- Documentation •
- Help
+ Screenshots •
+ Capabilities •
+ Quick start •
+ Deployment •
+ Development •
+ Documentation
+---
+
## 📝 Project Description
-> [!NOTE]
+New API is a self-hosted AI gateway for applications, agents, and teams. Connect upstream model services, expose a consistent API to your clients, and manage routing, access, usage, and costs in one place.
+
+Use it to share authorized model access across a team, switch providers without configuring every client again, or operate a private multi-model service with a web console. Upstreams include OpenAI, Anthropic, Google Gemini, Azure OpenAI, AWS Bedrock, Vertex AI, DeepSeek, Qwen, and other compatible services.
+
+> [!NOTE]
> This is an open-source project developed based on [One API](https://github.com/songquanpeng/one-api)
-> [!IMPORTANT]
+
+
+> [!IMPORTANT]
> - This project is intended solely for lawful and authorized AI API gateway, organization-level authentication, multi-model management, usage analytics, cost accounting, and private deployment scenarios.
> - Users must lawfully obtain upstream API keys, accounts, model services, and interface permissions, and must comply with upstream terms of service and applicable laws and regulations.
> - Users should ensure their use complies with upstream terms of service and applicable laws and regulations.
> - When providing generative AI services to the public, users should comply with applicable regulatory requirements and fulfill all filing, licensing, content safety, real-name verification, log retention, tax, and upstream authorization obligations required by their jurisdiction.
+
+
+> [!WARNING]
+> When operating this project as a public generative AI service or API resale service, users should first complete all required filing, licensing, content safety, real-name verification, log retention, tax, payment, and upstream authorization obligations.
+
+---
+
+
+
+## Screenshots
+
+Accounts, channels, plugin installation states, usage, prices, and costs shown below are simulated data. The marketplace displays the official plugin catalog. Click an image to view it at full size.
+
+| Usage dashboard | Plugin marketplace |
+| --- | --- |
+| [](assets/screenshots/dashboard.en.jpg) | [](assets/screenshots/plugin-marketplace.en.jpg) |
+| **Model square** | **Usage logs** |
+| [](assets/screenshots/models.en.jpg) | [](assets/screenshots/usage-logs.en.jpg) |
+
---
## 🤝 Trusted Partners
@@ -80,9 +109,6 @@
-
-
-
@@ -107,324 +133,174 @@
---
-## 🚀 Quick Start
+
-### Using Docker Compose (Recommended)
+## Capabilities
+
+| Area | What you can do |
+| --- | --- |
+| Model access | Use OpenAI Chat Completions, Responses, Anthropic Messages, and Gemini APIs; stream responses and use tools, reasoning, and multimodal inputs where supported |
+| Routing | Configure model mappings, channel priorities and weights, retries, channel affinity, and multiple upstream keys |
+| Usage and costs | Manage quotas, subscriptions, usage logs, cache accounting, and expression-based pricing for different usage tiers |
+| Access control | Manage users, groups, fine-grained permissions, and API key restrictions; use OAuth/OIDC, passkeys, two-factor authentication, and login session management |
+| Asynchronous tasks | Extend image, video, and other task APIs with JavaScript plugins, including task status and output retrieval |
+| Web console | Configure channels and models, inspect usage and audit logs, and try models in the playground; available in English, Simplified Chinese, Traditional Chinese, French, Japanese, Russian, and Vietnamese |
+
+### Protocols and endpoints
+
+| Interface | Common endpoints |
+| --- | --- |
+| OpenAI Chat / Responses | `POST /v1/chat/completions`, `POST /v1/responses` |
+| Anthropic Messages | `POST /v1/messages` |
+| Gemini | `POST /v1beta/models/{model}:generateContent`, `POST /v1beta/models/{model}:streamGenerateContent` |
+| Realtime / Responses WebSocket | `GET /v1/realtime`, `GET /v1/responses` (WebSocket upgrade) |
+| Images / audio | `/v1/images/generations`, `/v1/images/edits`, `/v1/audio/speech`, `/v1/audio/transcriptions`, `/v1/audio/translations` |
+| Embeddings / rerank | `POST /v1/embeddings`, `POST /v1/rerank` |
+| Task plugins | `POST /v1/tasks/{pluginKey}`, `GET /v1/tasks/{taskId}`, plus routes declared by each plugin |
+
+[RelayKit](./relaykit/README.md) provides request, response, and streaming conversion between the four text protocols. Available features depend on the channel, upstream model, and conversion path; protocol-specific tools and fields may not map exactly. WebSocket support also requires a compatible upstream and channel configuration.
+
+This README describes the current source tree. Check the release notes for the version you deploy.
+
+
+
+## Quick start
+
+### Try locally with Docker
+
+This starts a single instance with SQLite and binds it to localhost:
+
+```bash
+mkdir -p data
+docker run --name new-api -d --restart unless-stopped \
+ -p 127.0.0.1:3000:3000 \
+ -e TZ=Asia/Shanghai \
+ -v "$(pwd)/data:/data" \
+ calciumion/new-api:latest
+```
+
+Open [http://localhost:3000](http://localhost:3000) and complete the setup wizard to create the administrator account. The `data` directory persists the SQLite database across container replacements.
+
+### Make your first request
+
+1. Add a channel with your upstream API key, available models, and group assignment; run a channel test.
+2. Configure model pricing and ensure the user has quota or a valid subscription.
+3. Create an API key in the console with access to the same group and models.
+4. Set your client's base URL to `http://localhost:3000/v1` for OpenAI-compatible clients and use the **New API-issued key**.
+
+Set `NEW_API_KEY` in your shell to that key. List the models accessible to it:
+
+```bash
+curl --fail-with-body http://localhost:3000/v1/models \
+ -H "Authorization: Bearer ${NEW_API_KEY}"
+```
+
+Then call Responses, replacing `your-enabled-model` with an enabled model that supports this interface:
+
+```bash
+curl --fail-with-body http://localhost:3000/v1/responses \
+ -H "Authorization: Bearer ${NEW_API_KEY}" \
+ -H "Content-Type: application/json" \
+ -d '{"model":"your-enabled-model","input":"Hello!"}'
+```
+
+
+
+## Deployment
+
+### Docker Compose
+
+The repository's [Compose configuration](./docker-compose.yml) starts **New API + PostgreSQL + Redis** by default. It also contains examples for MySQL and a separate ClickHouse log database.
```bash
-# Clone the project
git clone https://github.com/QuantumNous/new-api.git
cd new-api
-
-# Edit docker-compose.yml configuration
-nano docker-compose.yml
-
-# Start the service
-docker-compose up -d
```
-
-Using Docker Commands
+Before starting, edit `docker-compose.yml`: replace the database and Redis example passwords in both the services and connection strings, and set a persistent random `SESSION_SECRET` (generate one with `openssl rand -hex 32`). For an HTTPS console, configure `SESSION_COOKIE_SECURE=true` and `SESSION_COOKIE_TRUSTED_URL` with its exact public HTTPS origin.
```bash
-# Pull the latest image
-docker pull calciumion/new-api:latest
-
-# Using SQLite (default)
-docker run --name new-api -d --restart always \
- -p 3000:3000 \
- -e TZ=Asia/Shanghai \
- -v ./data:/data \
- calciumion/new-api:latest
-
-# Using MySQL
-docker run --name new-api -d --restart always \
- -p 3000:3000 \
- -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" \
- -e TZ=Asia/Shanghai \
- -v ./data:/data \
- calciumion/new-api:latest
+docker compose up -d
+docker compose logs -f new-api
```
-> **💡 Tip:** `-v ./data:/data` will save data in the `data` folder of the current directory, you can also change it to an absolute path like `-v /your/custom/path:/data`
+### Storage and configuration
-
+| Component | Options |
+| --- | --- |
+| Main database | SQLite, MySQL ≥ 5.7.8, or PostgreSQL ≥ 9.6 |
+| Separate log database | Configure with `LOG_SQL_DSN`; also supports ClickHouse |
+| Cache | Optional Redis plus in-memory caching; use shared Redis when application nodes need shared rate limits |
+| Container platforms | Linux amd64 / arm64 |
----
+| Variable | Purpose |
+| --- | --- |
+| `SQL_DSN` | Main database connection; unset uses SQLite |
+| `LOG_SQL_DSN` | Optional separate log database connection |
+| `REDIS_CONN_STRING` | Redis connection string |
+| `SESSION_SECRET` | Persistent authentication secret; all nodes must use the same value |
+| `CRYPTO_SECRET` | Defaults to `SESSION_SECRET`; nodes sharing Redis must use the same effective value |
+| `SESSION_COOKIE_SECURE` | Set to `true` for an HTTPS console; enables Secure refresh cookies and strict refresh/logout origin checks |
+| `SESSION_COOKIE_TRUSTED_URL` | Required in Secure mode: comma-separated exact HTTPS origins, without paths or wildcards; leave unset for local HTTP |
+| `TRUSTED_PROXIES` | Trusted reverse-proxy IPs/CIDRs, or `none`; explicitly configure for your network |
-🎉 After deployment is complete, visit `http://localhost:3000` to start using!
+See the [environment example](./.env.example), [environment reference](https://docs.newapi.ai/en/docs/installation/config-maintenance/environment-variables), and [authentication and session guide](./docs/authentication.md) for full configuration. Configure container variables in Compose's `environment` or `env_file`; copying `.env.example` alone does not inject variables into the container.
-> [!WARNING]
-> When operating this project as a public generative AI service or API resale service, users should first complete all required filing, licensing, content safety, real-name verification, log retention, tax, payment, and upstream authorization obligations.
+For production, put the console behind HTTPS and configure your reverse proxy for streaming and WebSocket upgrades. Persist and back up the database and mounted data. Multi-node deployments must share the main database and authentication secrets; separate Redis instances or in-memory rate limiters count limits independently per node. The session guide describes propagation behavior for each topology.
-📖 For more deployment methods, please refer to [Deployment Guide](https://docs.newapi.pro/en/docs/installation)
+Pin an image version from [Releases](https://github.com/QuantumNous/new-api/releases), review its upgrade notes, and back up before upgrading. The `latest` tag follows published builds and can change; migrations and compatibility must be assessed for your existing installation.
----
+
-## 📚 Documentation
+## Development and extensions
-
+The backend uses Go and Gin. The web console uses React 19, TypeScript, Rsbuild, TanStack, and Tailwind CSS 4. Use Bun for frontend dependencies and scripts; see [go.mod](./go.mod) for the Go language baseline and [Dockerfile](./Dockerfile) for the container build toolchain.
-### 📖 [Official Documentation](https://docs.newapi.pro/en/docs) | [](https://deepwiki.com/QuantumNous/new-api)
-
-
-
-**Quick Navigation:**
-
-| Category | Link |
-|------|------|
-| 🚀 Deployment Guide | [Installation Documentation](https://docs.newapi.pro/en/docs/installation) |
-| ⚙️ Environment Configuration | [Environment Variables](https://docs.newapi.pro/en/docs/installation/config-maintenance/environment-variables) |
-| 📡 API Documentation | [API Documentation](https://docs.newapi.pro/en/docs/api) |
-| ❓ FAQ | [FAQ](https://docs.newapi.pro/en/docs/support/faq) |
-| 💬 Community Interaction | [Communication Channels](https://docs.newapi.pro/en/docs/support/community-interaction) |
-
----
-
-## ✨ Key Features
-
-> For detailed features, please refer to [Features Introduction](https://docs.newapi.pro/en/docs/guide/wiki/basic-concepts/features-introduction)
-
-### 🎨 Core Functions
-
-| Feature | Description |
-|------|------|
-| 🎨 New UI | Modern user interface design |
-| 🌍 Multi-language | Supports Chinese, English, French, Japanese |
-| 🔄 Data Compatibility | Fully compatible with the original One API database |
-| 📈 Data Dashboard | Visual console and statistical analysis |
-| 🔒 Permission Management | Token grouping, model restrictions, user management |
-
-### 💰 Authorized Usage Accounting and Billing
-
-- ✅ Internal top-up and quota allocation for lawful authorized scenarios (EPay, Stripe)
-- ✅ Organization-level per-request, usage-based, and cache-hit cost accounting
-- ✅ Cache billing statistics for OpenAI, Azure, DeepSeek, Claude, Qwen, and supported models
-- ✅ Flexible billing policies for internal management or authorized enterprise customers
-
-### 🔐 Authorization and Security
-
-- 😈 Discord authorization login
-- 🤖 LinuxDO authorization login
-- 📱 Telegram authorization login
-- 🔑 OIDC unified authentication
-
-### 🚀 Advanced Features
-
-**API Format Support:**
-- ⚡ [OpenAI Responses](https://docs.newapi.pro/en/docs/api/ai-model/chat/openai/create-response)
-- ⚡ [OpenAI Realtime API](https://docs.newapi.pro/en/docs/api/ai-model/realtime/create-realtime-session) (including Azure)
-- ⚡ [Claude Messages](https://docs.newapi.pro/en/docs/api/ai-model/chat/create-message)
-- ⚡ [Google Gemini](https://doc.newapi.pro/en/api/google-gemini-chat)
-- 🔄 [Rerank Models](https://docs.newapi.pro/en/docs/api/ai-model/rerank/create-rerank) (Cohere, Jina)
-
-**Intelligent Routing:**
-- ⚖️ Channel weighted random
-- 🔄 Automatic retry on failure
-- 🚦 User-level model rate limiting
-
-**Format Conversion:**
-- 🔄 **OpenAI Compatible ⇄ Claude Messages**
-- 🔄 **OpenAI Compatible → Google Gemini**
-- 🔄 **Google Gemini → OpenAI Compatible** - Text only, function calling not supported yet
-- 🚧 **OpenAI Compatible ⇄ OpenAI Responses** - In development
-- 🔄 **Thinking-to-content functionality**
-
-**Reasoning Effort Support:**
-
-
-View detailed configuration
-
-**OpenAI series models:**
-- `o3-mini-high` - High reasoning effort
-- `o3-mini-medium` - Medium reasoning effort
-- `o3-mini-low` - Low reasoning effort
-- `gpt-5-high` - High reasoning effort
-- `gpt-5-medium` - Medium reasoning effort
-- `gpt-5-low` - Low reasoning effort
-
-**Claude thinking models:**
-- `claude-3-7-sonnet-20250219-thinking` - Enable thinking mode
-
-**Google Gemini series models:**
-- `gemini-2.5-flash-thinking` - Enable thinking mode
-- `gemini-2.5-flash-nothinking` - Disable thinking mode
-- `gemini-2.5-pro-thinking` - Enable thinking mode
-- `gemini-2.5-pro-thinking-128` - Enable thinking mode with thinking budget of 128 tokens
-- You can also append `-low`, `-medium`, or `-high` to any Gemini model name to request the corresponding reasoning effort (no extra thinking-budget suffix needed).
-
-
-
----
-
-## 🤖 Model Support
-
-> For details, please refer to [API Documentation - Gateway Interface](https://docs.newapi.pro/en/docs/api)
-
-| Model Type | Description | Documentation |
-|---------|------|------|
-| 🤖 OpenAI GPTs | gpt-4-gizmo-* series | - |
-| 🎨 Midjourney-Proxy | [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy) | [Documentation](https://doc.newapi.pro/en/api/midjourney-proxy-image) |
-| 🎵 Suno-API | [Suno API](https://github.com/Suno-API/Suno-API) | [Documentation](https://doc.newapi.pro/en/api/suno-music) |
-| 🔄 Rerank | Cohere, Jina | [Documentation](https://docs.newapi.pro/en/docs/api/ai-model/rerank/create-rerank) |
-| 💬 Claude | Messages format | [Documentation](https://docs.newapi.pro/en/docs/api/ai-model/chat/create-message) |
-| 🌐 Gemini | Google Gemini format | [Documentation](https://doc.newapi.pro/en/api/google-gemini-chat) |
-| 🔧 Dify | ChatFlow mode | - |
-| 🎯 Custom upstream | Supports configuring legally authorized upstream endpoints | - |
-
-### 📡 Supported Interfaces
-
-
-View complete interface list
-
-- [Chat Interface (Chat Completions)](https://docs.newapi.pro/en/docs/api/ai-model/chat/openai/create-chat-completion)
-- [Response Interface (Responses)](https://docs.newapi.pro/en/docs/api/ai-model/chat/openai/create-response)
-- [Image Interface (Image)](https://docs.newapi.pro/en/docs/api/ai-model/images/openai/v1-images-generations--post)
-- [Audio Interface (Audio)](https://docs.newapi.pro/en/docs/api/ai-model/audio/openai/create-transcription)
-- [Video Interface (Video)](https://docs.newapi.pro/en/docs/api/ai-model/videos/sora/createvideo)
-- [Embedding Interface (Embeddings)](https://docs.newapi.pro/en/docs/api/ai-model/embeddings/create-embedding)
-- [Rerank Interface (Rerank)](https://docs.newapi.pro/en/docs/api/ai-model/rerank/create-rerank)
-- [Realtime Conversation (Realtime)](https://docs.newapi.pro/en/docs/api/ai-model/realtime/create-realtime-session)
-- [Claude Chat](https://docs.newapi.pro/en/docs/api/ai-model/chat/create-message)
-- [Google Gemini Chat](https://doc.newapi.pro/en/api/google-gemini-chat)
-
-
-
----
-
-## 🚢 Deployment
-
-> [!TIP]
-> **Latest Docker image:** `calciumion/new-api:latest`
-
-### 📋 Deployment Requirements
-
-| Component | Requirement |
-|------|------|
-| **Local database** | SQLite (Docker must mount `/data` directory)|
-| **Remote database** | MySQL ≥ 5.7.8 or PostgreSQL ≥ 9.6 |
-| **Container engine** | Docker / Docker Compose |
-| **System architecture** | 64-bit only (amd64 / arm64); 32-bit systems are not supported |
-
-### ⚙️ Environment Variable Configuration
-
-
-Common environment variable configuration
-
-| Variable Name | Description | Default Value |
-|--------|------|--------|
-| `SESSION_SECRET` | Authentication signing secret; must be identical on every node | - |
-| `SESSION_COOKIE_SECURE` | `false`/unset disables the refresh/logout OriginGuard for local HTTP dev proxies; `true` enables the Secure cookie and strict Origin checks | `false` |
-| `SESSION_COOKIE_TRUSTED_URL` | Required with Secure mode: comma-separated exact HTTPS Origins allowed to call refresh/logout; not a relay CORS allowlist | - |
-| `TRUSTED_PROXIES` | Unset/blank trusts loopback, RFC 1918 and IPv6 ULA with a startup warning; `none` trusts no proxies; an explicit proxy IP/CIDR list replaces the defaults | `127.0.0.0/8, ::1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7` |
-| `USER_SESSION_ACTIVE_LIMIT` | Maximum active login Sessions per user | `50` |
-| `USER_SESSION_ISSUANCE_LIMIT` | Maximum Sessions created per user within the issuance window, including revoked Sessions | `100` |
-| `USER_SESSION_ISSUANCE_WINDOW_SECONDS` | Per-user Session issuance window; clamped to the revoked retention period when configured higher | `86400` |
-| `USER_SESSION_REVOKED_RETENTION_DAYS` | Days to retain revoked Session rows for audit and issuance accounting | `7` |
-| `USER_SESSION_HOURLY_ALERT_THRESHOLD` | Global Sessions created per hour that triggers an alert only; it never blocks login | `5000` |
-| `CRYPTO_SECRET` | HMAC secret for cache keys; nodes sharing Redis must use the same effective value | Defaults to `SESSION_SECRET` |
-| `SQL_DSN` | Database connection string | - |
-| `REDIS_CONN_STRING` | Redis connection string | - |
-| `STREAMING_TIMEOUT` | Streaming timeout (seconds) | `300` |
-| `STREAM_SCANNER_MAX_BUFFER_MB` | Max per-line buffer (MB) for the stream scanner; increase when upstream sends huge image/base64 payloads | `64` |
-| `MAX_REQUEST_BODY_MB` | Max request body size (MB, counted **after decompression**; prevents huge requests/zip bombs from exhausting memory). Exceeding it returns `413` | `32` |
-| `AZURE_DEFAULT_API_VERSION` | Azure API version | `2025-04-01-preview` |
-| `ERROR_LOG_ENABLED` | Error log switch | `false` |
-| `PYROSCOPE_URL` | Pyroscope server address | - |
-| `PYROSCOPE_APP_NAME` | Pyroscope application name | `new-api` |
-| `PYROSCOPE_BASIC_AUTH_USER` | Pyroscope basic auth user | - |
-| `PYROSCOPE_BASIC_AUTH_PASSWORD` | Pyroscope basic auth password | - |
-| `PYROSCOPE_MUTEX_RATE` | Pyroscope mutex sampling rate | `5` |
-| `PYROSCOPE_BLOCK_RATE` | Pyroscope block sampling rate | `5` |
-| `HOSTNAME` | Hostname tag for Pyroscope | `new-api` |
-
-📖 **Complete configuration:** [Environment Variables Documentation](https://docs.newapi.pro/en/docs/installation/config-maintenance/environment-variables)
-
-
-
-### 🔧 Deployment Methods
-
-
-Method 1: Docker Compose (Recommended)
+Build the frontend before starting the backend, which embeds `web/dist`:
```bash
-# Clone the project
-git clone https://github.com/QuantumNous/new-api.git
-cd new-api
-
-# Edit configuration
-nano docker-compose.yml
-
-# Start service
-docker-compose up -d
+# Repository root
+cd web
+bun install --frozen-lockfile
+bun run build
+cd ..
+go run .
```
-
+In a second terminal, start the frontend development server:
-
-Method 2: Docker Commands
-
-**Using SQLite:**
```bash
-docker run --name new-api -d --restart always \
- -p 3000:3000 \
- -e TZ=Asia/Shanghai \
- -v ./data:/data \
- calciumion/new-api:latest
+cd web
+bun run dev -- --port 5173
```
-**Using MySQL:**
-```bash
-docker run --name new-api -d --restart always \
- -p 3000:3000 \
- -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" \
- -e TZ=Asia/Shanghai \
- -v ./data:/data \
- calciumion/new-api:latest
-```
+Open [http://localhost:5173](http://localhost:5173); the development server proxies API requests to the backend on port 3000. For a containerized development backend, see [docker-compose.dev.yml](./docker-compose.dev.yml) and the `make dev` target in [makefile](./makefile).
-> **💡 Path explanation:**
-> - `./data:/data` - Relative path, data saved in the data folder of the current directory
-> - You can also use absolute path, e.g.: `/your/custom/path:/data`
+| Location | Responsibility |
+| --- | --- |
+| `router/`, `middleware/`, `controller/` | HTTP routes, access checks, and API handlers |
+| `relay/` | Upstream adapters and request routing |
+| `service/`, `model/` | Business logic and persistence |
+| [relaykit/](./relaykit/README.md) | Independently buildable Go module for protocol DTOs and conversions |
+| [plugins/tasks/](./plugins/tasks/) | JavaScript task plugins; see [Task Plugin API v1](./docs/plugin-api/v1.md) for authoring and host boundaries |
+| `web/` | Web console; see [frontend conventions](./web/AGENTS.md) |
+| [electron/](./electron/README.md) | Desktop wrapper and packaging |
-
+Read [AGENTS.md](./AGENTS.md) before contributing. Run checks appropriate to your change, including `make test` for the Go modules and `bun run typecheck`, `bun run lint`, `bun run test`, and `bun run build` in `web/` for frontend changes. Changes to RelayKit must also pass `GOWORK=off go build ./...` from `relaykit/`.
-
-Method 3: BaoTa Panel
+
-1. Install BaoTa Panel (≥ 9.2.0 version)
-2. Search for **New-API** in the application store
-3. One-click installation
+## Documentation and community
-📖 [Tutorial with images](./docs/BT.md)
+| Resource | Link |
+| --- | --- |
+| Official documentation | [Guides](https://docs.newapi.ai/en/docs) · [Installation](https://docs.newapi.ai/en/docs/installation) · [API reference](https://docs.newapi.ai/en/docs/api) |
+| Project exploration | [DeepWiki](https://deepwiki.com/QuantumNous/new-api) |
+| Questions and discussion | [FAQ](https://docs.newapi.ai/en/docs/support/faq) · [Community](https://docs.newapi.ai/en/docs/support/community-interaction) |
+| Bugs and feature requests | [GitHub Issues](https://github.com/QuantumNous/new-api/issues) |
+| Security reports | Follow the [security policy](./.github/SECURITY.md) for private reporting |
-
-
-### ⚠️ Multi-machine Deployment Considerations
-
-> [!WARNING]
-> - All nodes must use the same primary database and the same `SESSION_SECRET`; otherwise Access Tokens, refresh sessions, and temporary authentication flows cannot be verified consistently.
-> - Nodes connected to the same Redis must also use the same `CRYPTO_SECRET`, or their cache-key digests will differ and shared entries cannot be reused consistently.
-
-The database is authoritative for login Sessions and for the per-user active/issuance limits. Redis Session entries are short-lived caches whose TTL follows `SYNC_FREQUENCY` (60 seconds by default) and never exceeds the Session's remaining lifetime.
-
-| Redis topology | Session propagation | Rate limiting |
-| --- | --- | --- |
-| Shared Redis | Revocations and version publications normally propagate immediately | Redis limits are shared across nodes |
-| Independent Redis per node | Nodes converge from the database within the effective `SYNC_FREQUENCY`; a newly rotated token may receive a temporary 401 on a node with stale cache | Each node has its own allowance, so aggregate capacity can reach roughly the configured limit multiplied by the node count |
-| No Redis | Every Session validation reads the database | In-memory limits are independent per node |
-
-A shorter `SYNC_FREQUENCY` reduces the independent-Redis staleness window but causes one additional primary-key Session lookup per active SID, per node, per TTL. These guarantees make Session authentication bounded-stale across the supported topologies; rate limits and other Redis-backed control-plane caches remain topology-dependent.
-
-See [User authentication and login sessions](./docs/authentication.md) for the token, Origin-check and PAT contracts.
-
-### 🔄 Channel Retry and Cache
-
-**Retry configuration:** `Settings → Operation Settings → General Settings → Failure Retry Count`
-
-**Cache configuration:**
-- `REDIS_CONN_STRING`: Redis cache (recommended)
-- `MEMORY_CACHE_ENABLED`: Memory cache
+For bug reports, include the version, deployment method, reproduction steps, and redacted logs. Documentation, translations, provider integrations, and focused regression tests are all welcome contributions.
---
@@ -446,25 +322,23 @@ See [User authentication and login sessions](./docs/authentication.md) for the t
---
-## 💬 Help Support
+## 📜 License
-### 📖 Documentation Resources
+This project is licensed under the [GNU Affero General Public License v3.0 (AGPLv3)](./LICENSE).
-| Resource | Link |
-|------|------|
-| 📘 FAQ | [FAQ](https://docs.newapi.pro/en/docs/support/faq) |
-| 💬 Community Interaction | [Communication Channels](https://docs.newapi.pro/en/docs/support/community-interaction) |
-| 🐛 Issue Feedback | [Issue Feedback](https://docs.newapi.pro/en/docs/support/feedback-issues) |
-| 📚 Complete Documentation | [Official Documentation](https://docs.newapi.pro/en/docs) |
+Additional terms under AGPLv3 Section 7 apply. Modified versions must preserve
+the author attribution notice `Frontend design and development by New API
+contributors.` in the appropriate legal notices and in any prominent about,
+legal, footer, or attribution location presented by the user interface.
-### 🤝 Contribution Guide
+Modified versions that present a user interface must also preserve a visible
+link to the original project: .
-Welcome all forms of contribution!
+This is an open-source project developed based on [One API](https://github.com/songquanpeng/one-api) (MIT License).
-- 🐛 Report Bugs
-- 💡 Propose New Features
-- 📝 Improve Documentation
-- 🔧 Submit Code
+If your organization's policies do not permit the use of AGPLv3-licensed software, or if you wish to avoid the open-source obligations of AGPLv3, please contact us at: [support@quantumnous.com](mailto:support@quantumnous.com)
+
+See [NOTICE](./NOTICE) and [third-party licenses](./THIRD-PARTY-LICENSES.md) for attribution and dependency notices.
---
@@ -484,7 +358,7 @@ Welcome all forms of contribution!
If this project is helpful to you, welcome to give us a ⭐️ Star!
-**[Official Documentation](https://docs.newapi.pro/en/docs)** • **[Issue Feedback](https://github.com/Calcium-Ion/new-api/issues)** • **[Latest Release](https://github.com/Calcium-Ion/new-api/releases)**
+**[Official Documentation](https://docs.newapi.ai/en/docs)** • **[Issue Feedback](https://github.com/Calcium-Ion/new-api/issues)** • **[Latest Release](https://github.com/Calcium-Ion/new-api/releases)**
Built with ❤️ by QuantumNous
diff --git a/README.fr.md b/README.fr.md
index 7870076..583c6bc 100644
--- a/README.fr.md
+++ b/README.fr.md
@@ -4,7 +4,7 @@
# New API
-🍥 **Passerelle de modèles étendus de nouvelle génération et système de gestion d'actifs d'IA**
+**Une passerelle IA pour les modèles, les applications et les agents**
简体中文 |
@@ -44,23 +44,34 @@
- Démarrage rapide •
- Fonctionnalités clés •
- Déploiement •
- Documentation •
- Aide
+ Fonctionnalités •
+ Démarrage rapide •
+ Déploiement •
+ Développement •
+ Documentation
+---
+
## 📝 Description du projet
+New API est une passerelle IA auto-hébergée pour les applications, les agents et les équipes. Connectez vos fournisseurs de modèles, exposez une API commune à vos clients et gérez le routage, les accès, les usages et les coûts depuis une même console.
+
+Utilisez-la pour partager des accès autorisés au sein d'une équipe, changer de fournisseur sans reconfigurer chaque client ou exploiter un service privé multi-modèles. Les fournisseurs incluent OpenAI, Anthropic, Google Gemini, Azure OpenAI, AWS Bedrock, Vertex AI, DeepSeek, Qwen et d'autres services compatibles.
+
> [!IMPORTANT]
> - Ce projet est exclusivement destiné aux scénarios de passerelle API d'IA légalement autorisés, d'authentification organisationnelle, de gestion multi-modèles, d'analyse d'utilisation, de comptabilisation des coûts et de déploiement privé.
> - Les utilisateurs doivent obtenir légalement les clés API, comptes, services de modèles et autorisations d'interface en amont, et doivent respecter les conditions d'utilisation en amont et les lois et réglementations applicables.
> - Les utilisateurs doivent s'assurer que leur utilisation est conforme aux conditions d'utilisation en amont et aux lois et réglementations applicables.
> - Lors de la fourniture de services d'IA générative au public, les utilisateurs doivent se conformer aux exigences réglementaires applicables et remplir toutes les obligations d'enregistrement, de licence, de sécurité du contenu, de vérification d'identité, de conservation des journaux, de fiscalité et d'autorisation en amont requises par leur juridiction.
+
+
+> [!WARNING]
+> Lorsque vous exploitez ce projet en tant que service public d'IA générative ou service de revente d'API, les utilisateurs doivent d'abord remplir toutes les obligations requises en matière d'enregistrement, de licence, de sécurité du contenu, de vérification d'identité, de conservation des journaux, de fiscalité, de paiement et d'autorisation en amont.
+
---
## 🤝 Partenaires de confiance
@@ -79,9 +90,6 @@
-->
-
-
+
+> [!WARNING]
+> 本プロジェクトを公衆向け生成 AI サービスまたは API 再販サービスとして運営する場合、ユーザーは届出、コンテンツセキュリティ、本人確認、ログ保持、税務、決済、上流認可などの必要なコンプライアンス義務を先に完了してください。
+
---
## 🤝 信頼できるパートナー
@@ -79,9 +90,6 @@
-->
-
-
+
+> [!WARNING]
+> When operating this project as a public generative AI service or API resale service, users should first complete all required filing, licensing, content safety, real-name verification, log retention, tax, payment, and upstream authorization obligations.
+
+---
+
+
+
+## Screenshots
+
+Accounts, channels, plugin installation states, usage, prices, and costs shown below are simulated data. The marketplace displays the official plugin catalog. Click an image to view it at full size.
+
+| Usage dashboard | Plugin marketplace |
+| --- | --- |
+| [](assets/screenshots/dashboard.en.jpg) | [](assets/screenshots/plugin-marketplace.en.jpg) |
+| **Model square** | **Usage logs** |
+| [](assets/screenshots/models.en.jpg) | [](assets/screenshots/usage-logs.en.jpg) |
+
---
## 🤝 Trusted Partners
@@ -79,9 +105,6 @@
-->
-
-
+
+> [!WARNING]
+> 将本项目作为面向公众的生成式 AI 服务或 API 转售服务运营时,使用者应先完成备案、内容安全、实名、日志留存、税务、支付和上游授权等合规义务。
+
+---
+
+
+
+## 项目截图
+
+以下账号、渠道、插件安装状态、用量、价格和费用均为模拟数据,插件市场展示官方目录。点击图片可查看原图。
+
+| 数据看板 | 插件市场 |
+| --- | --- |
+| [](assets/screenshots/dashboard.zh-CN.jpg) | [](assets/screenshots/plugin-marketplace.zh-CN.jpg) |
+| **模型广场** | **使用日志** |
+| [](assets/screenshots/models.zh-CN.jpg) | [](assets/screenshots/usage-logs.zh-CN.jpg) |
+
---
## 🤝 我们信任的合作伙伴
@@ -79,9 +105,6 @@
-->
-
-
+
+> [!WARNING]
+> 將本專案作為面向公眾的生成式 AI 服務或 API 轉售服務運營時,使用者應先完成備案、內容安全、實名、日誌留存、稅務、支付和上游授權等合規義務。
+
---
## 🤝 我們信任的合作伙伴
@@ -78,9 +89,6 @@
-->
-
-
S)
zv?NnX
z8?)!a0|Q}*pRr-na@oOOqhlOtzs?$q4}-VU5}f!9n&ZpwbHtMfk3F*UI5Pi+B+0%z
z0;9kUwU}dLGkF*-?D{lPOxx;2N_BEhaRyliv|%6LYhk
zlOIyQbdebO9}GZR4BzpjC!c2;f-N`J2sz!elyp%+VUdejIWd0)OND_M**)|oxs=4J$03B3@b4GStnjh@hz(R4yzYTUG4yv!lYuNUY7_fkL
z@8x_^!HU)R-d@4f&NT4|6pNmwViM?cYvhwfA*nmE#u>L_n>w#}R2J$#dSZVPgrrpF
zd~^^7P!C_%rbck9
z4L$bS4=b^+cJM$05xHb0{MEbVNiOf{0hg8IF9R40v}~^E_`&fHr9S)c$Bjp;+yz#U
z6+8HPF0gOS22frg(Jmo!(Z(VX#XR3{^hIf=KWK$pgQ(z5MA#imGWjryub)D!qD7CeQPuF3G>CeN&JCYkM(!iYj{h0(qOiI5z*Zj
zC|lJu;fTvgeSQb0HT@yuUCIrSwlxF2=Py1F>Uid}`&Gb@QpLWl2+9>6IWzGmV;K
z;YF#XRjB&d_$sa{Tr#r0uY_lHGs^tFE8
z43gmIOFVd>d$tB`TaGK(D<$}pj>b6*ps^|(U}W%G{#CxK?(BwSDHakbiLpcLRT|Vf
zERvc|Z0a|%lAm^XHs}^PSwD?b5_40yegvfPAiR?9IPWxSz0&iCIvK;(r;Qvwcnf`6
z^e7!WgK2S}tgGaIA?t5e*Kur`p!rYqjpQs>U%ONqV)68(FWSB(E`+P5lL<$x3ek4E
z^{JauuO9vem{#l8iM6k8(s`FRrp`%bO&rb-^w1^Ohdck;BQZ9fX$TN*PenB+i&W6#
z!Idp`%1m>Wp%pCMm(pk+>2Ph|0`^&DmuaA#RG=4llb?Ok4mPDUZ**+c*Mzw3$
zH}U03z0f9|dO3Vp&|0Xl(NCS161RyonGuvGW?Eiv_3Vef4=4CED>t2uk6x?BCvRh+
zlZ2HZ!qW%U4=$gxozF3=VWO<_PMD&bR=oQys3$w*5a!ADHYTUF$ROR3R;7nsZWT2g
zVkI2|b>Y_qzRNhvc6RFNsq_3YRtGFCcaqa%x2;=`Lh4Q-W4{Ky__p4tTRG~K0#Vz7oRCWp)gH#mB^
zAx>nK%bDPg(q0}3LzdZ7Mpgs{)8kE6q=
zx9E$VDr6Q4jzaNmZh9Z@hm=uU8FWDcHGz8jh_)JoXQVo^FhQ0jhAqG=LonZ^8jMfA
zl*v;rbKYJ($;2av4buGBywPcy82Sx3Y;G{t-f?v7NJ~_lH(hP9jIUOudAd>s-MFqg
z=HBZw8?Amh)?0o^;V_M{fZw?L0H1Eca5Pk9s5mO2dlDJ3jSf@C=7g`cRcDB``ZEcY
z{$`68#@PI9v3kpRfbLPZg4w*GSArG#`#$lQmoc
z52c(hlx3yUZxdOS6j?~H-cZ!h&V_LFcLY)gx?;2bIbVjiPfaiC`JH=6l_1ozk7g+a
z<;U`Cn#f<64oMnr3J#T(v*b4)V8mlYl)o3_iY^>5`$|@V*RnCV!mCh0u88867#VDF
zhO7J1vJ*GnyorbFt!Qze&rB|0k}sND*t2Ms{;f!a#!;?0w1&r9udtk!n$YtQ-H-%O
zTR(vtM4WrU&=Wdz`s5-5N54e|2+{aj21!W1kF1SY$N`f*r4D67qwK8%M}`(^HFp;C
z8vs|j9hjpfjzFd;hgm6vmwHAj%FP91NI-;l2-ia~73pvBsSWREWMxflSLgHSr3cPd
zckf*`r(dX4B6SfcHLT30^(=6nc>BBJ{0uQl_Hk}oov3n#V@rdSfF6z77oqn*i`F_mh{VX&O?Y=mnj8vb^6YEC63VDEVC;fpmD
zC2`v~s1C+SN;&h%$n+^mzDF-E>P25~@m_}O!62wZEs@ojtN!8EZ-CKj7N)b6ul3R?
z^5MIUc(lgx@Y-4$JVC99cFhZ)bVKN18E)A~Z}LP+(MbiEFIstR2G*?+?iY6Y0+5J9
zbW*;MlUR#XlYs&@axh74*mwDIq8ga
z$~iDOy%!&_Q7XD$b$VF68u&_(-i#uFAOX7M})^CA20Zl>Ka}CaCY=s
zHE0trHEpTJ66w1iQ`_H%2ae{Oabc-sSBAt=XAB=-F1
z$*e+0KIRNW+zaS<&`xD?>Y>L8F*{*vR!GUBbad)RbeIylbOd97EZY0iFZYswWs0Vg
znek-F)1p_qCZpbD;))!og^|GmX|D#)c+X+%>%|=M1$(Nw{aJpll-1xj2GR=}ERc<1
z%C_@V+aGhjc`lhGz3j>xcqoW7tTlGy073HE$j8BTU9oq}w97@P5uEM-KO&td>e#>@
zz)|tmU1L-T)IhTiIu&rhvgt%i8C~IEOg1%@-RzPW_-5m=1rS8|0CCnLB>%K|_~GPw
zqIU%PRSwbmOrGJ%oIaa$3mYTLQL2IK_^>)Bs20v^dS-6wrS3EmUS%NBv*hyY;?~K*
zV%SEL-lCo35_1%iL&?`4nY9grmS1i#W9v*@r;HtYyoYV9xuI*d(7Hf~A-G@a$(pdT
z<@=x=>*YV~{85^CFApy_1AUFHkYmz=p5CR8_=1oV2OrCkQ)8Tgy|_1j=$E~JS$)lN
zHD?mr|9+lD5z9pn&2mm#g%e8OO;(2M>4Fbv+6~8+zaEw1cahT2yzagyqPV9l}>Q
z4Hx%}SWzL%Q}4%PF0UxJYJ&-~r6tY|l{-hymvSp>=!r`oQRtcnqsNtN_|7RxK4
z6Ef}Z&l-~_qiqx-fgmvjUu#f?mE4@~sn=y0I#3u;pXIk=ClP$Yq+n)JC1ucaV_R*^
zZrti(_oA}7!3xo=H-8-!%t0t{aM89mci6|_B)b?Ne_E?%IB+|)6z_Sa>w|l~%eUqA
z_uXeR9gVy4VKpRRq?`5Y0+;sB&}+|4B#TbEKE0#n|2VIDBUL_UG5X-^{swAc&fHo}
z7d<(+Y0s>%crwBZeCl3*GGg}YS7tG`A7SIP&{Wxx>qbVDmWQcuHLHBfgJVV_PS+|s
zQtNaPXZkC7SlOSd58g*v3+!kw`H=8ns7~=`&GS*?$)tMVv?wEDGDu+G+$fs*F#)Tl
zLhO}WPD={Lg^Y_7-+aH(e54MdhhI9f`!C4oZD{zc7=xtG7@b1$D{0F4`Os;v91kgrvx|PVomz(`E$PvL(=4pvk^0h9J{oN{
zVbAk_668`Ko!~#LTpp{Vq0_F1?d%YkVxY%Jh#O`1GWK+6gXyB>@DT)7eZv01nt>~+
zV?4jtzlZQay&$eMF=?rL<|Mv4suqS;Ds)=%_hA>JLlVcc+M5S*YpyN}L;v|sc0RN$
z<8zsgWSn&Y`RwW}Ov3(RztM5SXxXWv;HK#oP!xnUhXfYJT*zAfbm3?TLBwp#&>6fi
zu{heONiLhc-VVo1L_*ZjVOtZ}jIsQ)6UF>kQYJqimVj2m*pL
z-Im^!*k#7NHbUiG*zAYUTlh1n{rnTfy6X
z?b^W-?MIJ`uFq`bnX1dfw4V3V1uHgX_y#xq;!m>%>0V%u?)BXXo-p*oJWtfc7v;2I
z8ylpqj-GXIgbiZ`Qjv;2J7J*db*aIG23E2C#*(``at^jVG_2k;A*yPTgAOHC;5a80
zq!wiNr3RKiEJE=0Paq0MM)(&tEuXH|SWdDfR5c7HEV7^x`tTHE(7LYY+0j9qhsH%8J59Zf5hIuCNML@S(Fti2>XPmP+TubHYV)qXVF8L6OKrWAah@!lt-`I>gD
zlp|!q4M&HlEMI1x=KOF}Y|Ew^?mUM^-r*I3`bk%$QaxGeACp_-xcx~WBPm{k%J3z{
zqI2b8!9}bgCa|y#3K{X*>xgtjvf2}9uwEs!5yqZBhtj{i^VB?~xlm$A{+MYXd#C4v
z2)$u1*&1(UZnuImc68qRH=sA~m8oMCNs4#x_&jd+1?`|S7Ai02+n-Q|`s3g0A+$Y|
z%9I`Im2pVy+%=%XR8aHT;}4AT5YNpocVDrQ;()=sd`dGVah@(^#WYiNDyJ|1V1y05
zuK8mbc`i-PC#vtM*uKN)q<0|M*r-}D1UlP~xFe=x!+mR4Hme6>;%YH&LGezb50Ttc
zrk8DP*vk^o*z@N0OXF?y2wi8127N4oHwW)2r8h}i33A;Nc=)GUYM7MwPSsBb&N$0m
zibFmQ((c@YMzhW3&-03@YiXmHFj;o4YWg^)Cbp^
z8Huq=dfY*nd-bufCdsx=m!+Q|aPT|*W`x<>438hN_Rhnh;Cct5Cq80iBhOX|W_lTM
zegb_6=la2oHx-PsJK%mMzUAyBGzWZd1$`S
zh(9Bou8&T!$D*a$=UFeX0sdi40q!GS6-mUVj7{iCUWwrxau?;EP#G(a-p;y)PHfiK
zU-wv~zo$N|J%gJEtr)<5175XzT(fz`1Bh*D(*J9QU0r=EHOpl+GGS!@-b&M+#viU$
zFQjnZD+k&|YEN?>UVJ8G_80ssYtMhqqPc2?$dbqFzh(y`hJUvIv~v>f4lv~Zn*CLj
zl?wm(&ukQWg&Z-lavFC}HzV(_HTBMT+>OT^
zd$9i3SnkjhJV9zO?6xB`UOD?Xji(NLtyHH>tF!f4v+s7G1cOLi^fPTZGp1YBOewr6
z#~xgt_j)F&;@x|x}l@;
zq~!8Fwro54UrLq=$f_&uIoOSkniQPu2+oCKYFY_VJgdjK`Tsm0HyCZ3sc);rGi~1M
zgsR{GO4%n;x}V+OW{?rpm)rf{$^YiY|ACX?gw2QH6;`an$8mrnFsk6}*77-4`{ww+
zyNZCMp!SQWt3uC2~^_)Jvw>*0qs+496!vrW(7Pruq&KYjf)6{h#O
zHM*m|Bz{SXY_t?Km_;Na+IdQj%WC=!=nrC4
zh1cz~|L(9+*0Ybms}WNMp=FEK^vi|}kO=wonp9MsWko{cRn;%|Q5Q!pdb@nNDjt|z;|X%8N9EBFMWO1M&J`a0%QB}p9+iC+YTaJ|a
zrFkarRp0hPV4B^v1SzbX(7ZMw)3%f6ZmmZsvBK}!*@>ejfOEzv=AB}nVThhwEqb%&
zLP28KonqMq%Ix^DBTk?lK_^Xf2(L|mdCvP9JLQ0lX!{;QmEP*W3q->M6789_CXwxS6)TniayHKpJU<>h}<9SlP%;dP&mRw*p~X`W4E1
zRE%SSs&Mi<7S;jwnfjtU3|f>z3d#;jj+@e7(Uo%Xzv(65_7-luIgo_1f5~?P8Cxl%
zOl`?#%#|Alx}r_z^)oG^jfW-^@mI7prAtan3Tfr}fnJ14c$i;Ip-#P$H_1?;2Hhls
z33Q4u-Q9bTP?;>np^wEXV&oZxf|x|vH^GpikS(%=yi>!zn`ajdWqO)5iQLbjARDp0
z!9xK{Z|&KIzXC74%2b(t-;>!i(5W;b6_ReQBs=10Fo1zT@bm(Tf!fdFt}~yn6vUQn
z-SYQ86>QiY$?)6=9+U3$^8;PzU~%ybf6TAf-EoUIqYRYiBR0c}Pf}#i3AG73lDJW>
zbr>$P*3pcG+4z{msh>(+cy5bFij&tI1GVYVVi_byhqFX1n9y-zmn5Yu~$R)L#1P3U{Hpgs2Z
zy+_WAKTy4t1H0`FhG{7hkh8MoQsB|<@-(F{ehv&7zAEz0OjNsk^^g3)(JJ#&`pgSy
zhxg#~X%uN)m-Qkl*Fjj?D3>2t@zQ}-H$1m@28bX8vDz&4dll*tW&Sw
zK?6Z@vsOZzzcLQKR6jnMVwM4VR3KjRid9BHaXEtc)
zcRMiNR;5sus9BtRdxkXRV70B+Ced6^Gqtv|*)*)sEdqr?!+GPG?7WwFhWT0yIOp{-
zxmGM+E&WTsn3eo^`dA}Zre!>N=(-av$Y$`_zQO~kLjl#`;ET%$5S_=V!)p7O@qSk@1;+&B9wsj
z`~4axrB5Wx>a=C&_qb3lER?{xz|c^&oHM$g0}}^1VeG<_v38tPrF5)?cS-w`3wbui
zx^iG9UCC>4lwaAC3HN|Ner5d|+5TTC!%>`DH|X?Ny!{0L0GmEFgytCSoUyxu)s|K<
zy=Zv!V243NXCb&5OvUKt6nyh|47#X)2sgRAh*6v+{<1)1WTww~{ubMI-C)4!O?lXc
zXj#xo_>Gn4=c%lh0PwKji)mq-$Gz^Nab5Ln=u4g=M$UZ#7{o-O`OUqdx(~P`H82=n
z@TF5(kappNx5JVgxl{muO&^2BiQbGk`xh^TKD4QqJ()CsE`1>8=bZ9ShgV@DOsy)4
zFADz7>%%9wjf2*A-#UCcD-8gAsZAg;OL2jh%toN)U8Le-G{3+37xI1S(P=55VS1i-
z2@0>_G{m9f)$ON1M)!(@W*ILpFK%fLuspRRWyy>46zTe90kn9qYX76cwo4;kQV+gf
ze6v~q-n{mYVXASSi4CMe{GaYLVFbnPSviY;}qrQ3NK-vN9aC
z$+Ss+_U)z-+q3#O)3DXt20XlQ0nLWNuJU`Ue1Al0D*LoFlUJ`T8~a7sbF4CSJ>Xe<7r<;0mHPxI&@f}
zNamQ=Zaq9+tLQOxWa3bGW^2ajV0tOb_U?Gu(@A;&pY9>K)sO7eMxzi?;=FfV@urM_
z;xnOz;2m;mMJ2&1rvm5Ej%_3=Y_aKtm2-&MJ+a5F{5-pJa2p%jc{KiF+bAq_7;nm6
z0z_S2R@~dFsI>apkU;tzcLMohocksh;`;6HbLGDz!ct^}K6>(3ynD;)^_(0Vn&%y*
zpNbsxI+?OJU&9LIrzldt)-LzuWxG{zRZqvpuVjU1iz3;C0AGC0xkRsV>5N%fy{EU$
zY7p&qcDg?J;I1*%8luC-(Ig*C)F%3DOBf=RsSiu$^_wXs&a4^+vW+y=`CpHvA5ea!
zt{Ks6q3l+-c`W38DcD`+aYXr$#wnRje8K(>zt(hjoP4!lwf4k?*F-c|oBnBM8zUrs
z?Qnxo$8mXG3+)w&p;#~>SI8P#2Wr3Ml2h*!TW*fFBu17UAfvTvSASeU>kVJppry~~
zfu1Hq7q2vcW$SayD@uD4Ue1R^gX9z@+tJs#yt1j^NyvXk$tUASjXwIV7LC
zbLg4BIH9?rZhyY@G4r+2A&1)N!n|%P4ei3JuC^+Qnmh5iCB#5_-B{K^H`!Y@VS8Vw
z0;A>0atO4~1>~JA$t~R&62)K3@TEqwk;PlHlYrkFZt`StMY`g}lkvbOIW7y8hVexu
zINgqHmxT$RDBk>cufY41OiIxVVPCAp*s-WxC)yW
z3oZr(?;NYdcN^Y%mGvV?FWKrahoPe2bgHZZ7*3uDE3vaYa-P{>|EAAewta@vuQH_1
z2jR3ei^IAaAd$`TBeMK>xKLv%ZywIn{RXfK}05PCv-xK1%eov$7`c
zYsM+PY{cn1j$XcYk_i3rbyUEcorsjqb0LR;O31UfPA+BH7y(3EoRKc4pZI4KFe}
z+EZ70ueu#vscQ(TseM~K%A=(@dWE*(o@i*%npVsVsn`o|l}Ls_*RmVHmFu8;r*mSi
z4%!>$N5h6qpVVNKGvNbsHRLf<3~P!i)Q%sZS4a=qsCK9e5@@@r?QhR!1&`m?PN83z
z0M|~CKVp@$D{kyQ)BEJ(f^-#JFQ!rAx)SD=CmQrA#1Um>AvYI>4-P9H^UV3XF!+Zn
z8DP6ljJ8&Bbn*33D$MASt7z~U=jT6v-5&m$`y23~n!bj=2a#*TVtQb%nwyoxXysqPOVegpn^@!qH;pyOClSX$!y
zD7csj<4HM~QnfD+5@`X2l#&PH6>!jc!U7}omya+N%^#4l>rs(Tn9sL%pp&`LS&8hn
zk>*a#PZ2$hnvK!ya~Lax(UWt??N#S%)AcV9!;O#TUVNu#s9Qvyw7V0iySmw6bWN_L
z;|TdvYWM2(0B>Ttn!=W-W7_6wa;Fjl-U#be3{JX9b5hI!L-P9heqQ6LaJffb^cM^D
z3YtvAW;Bk3a0|)185w)k>dB8Iz?T74DyWV4YnOjK>Eb#aEWgHa&umc1YDU69!5TI8
zNH|h>(aWk?wTrMGgDe&wkkuC{_zlo1UG3l3etAWy70S_eJ#P(L@%Es{7<|eZCCUF`
zpb%kd2X+H`cC(Vb1Pvxs5(?pk8Xr(}RDOE*H(*FhutD0{UIQla0iNbHd
z`D%igIw!{nn||O@0^_vPe63Hi7-NE3i1oy!1Wc@=pE-Y8h$#jP@u4RC~iPLrG^k)~Y5XjEGu&hLP@I
zKV1?8x+R_xNdQL&9uL&sPA5CfU-KsSrstK%O!5-v`IEIKAC+#t|9Vo3RGwvfL&_D$pxRCXx&1WGB9@Ti@LY2*IP3L_eNVl#IrPZw(Hhs~Ube=)9?)c3LYtp<}?#&{%{*dJ3S`WxQ}R7
z-^JyjP2J<6k(%)I3Iuxl=OOkh%F!ZR{?wKmwbrY}D@Ky0Dx8h_
z>qSGEW^+U#DWeGpqE{|k_u}0WaW25yUe-gYlBI<&)Q@IO8jdeb?Gsj!$A64F#XXF#
zK*zoTLzEuUGixYIm(HB$5lQxe`oux6k>`3n^hngA`W>Qs8-%w{+;yD2%MVgg}3WHSbM6=&*oMXC+1O
zgc+VW=Pb}S2ffGzzjO?{CfD0t);ree#h+PQuL+ejF!oamV{I6Wl3PQ}LJMCUdZ}Kk
z(eBlT?cOZ^c5I$L$JI*aC5AePFtL4Y@YTu+l^iY0E2nqX>rf34hMIWx%&?}M)9mAd
z*$y?ki&;|m!HcMwfsRWtkb3((pWvd~rVXlF4y33$jpR`l5$8RO;eV-RPTuc3D4RyLvM^scO35}-j5vNbRhRnn4m
zjW|)SddrJ5joL%;(ELF=cxN9aXYals|2=ei>*ENo(*?N=aH6^Oy!W&MtCZP9>BYV6
z*l82QuJ6bBcrkLbfgpX_Z@{^=s;3Ac
zWj}@|x3BDJD9l1JkDizUZ+w`P8~iNph<)qjO{0VN5a+DB4q^Qq%%)iw{PHBDYUojv
z&yeL&0o7=?J(?aH{e`N!4oMHlR{xbOFMwZtY~)Y4BXhd~SW#%|G*?7sYx&xVp%Xvt+*IqY
z?jcmxGUOdRky}cOm`wAt`~n>%WUB=^s5f|gO6wIk+XyL6_ts03aw<@OgIs0fPt9R#
zl(}uMd4#5;W!c7uLN+M6CJD_VXxonFz+7P$0-^K+-Xkp9V#rIQw3L;~vDl;Ln(FLR
zsm)_HP1f-Q4>x0F+zQ7XG%*R9zGz$3$<11QvGA!qpWZ>7(Jrao_|Gz48@`yuEB9!M
z2+f@@A5>WhdwCSxVnYnTZ@)k^YgLzo28ZZ*fus0*m0M@N=+fMcx6hI?%qAe$+&hEK
zVIj#sZLcH^BDgbLiHq-|;z2T}2`D`NTi*g3;p5`3I;z3}0rdqNsrNF*7m`I8LDd$P
z1Ng70_A+NPU87vg`A@WC5NYN&FSh8G`IqO@BJGEV&yf{1SWP*Ec{5rYx`D28|K+9F;(!5-
z2kgq4k*^uuWeXW*WlAdjWTz#!I`g0gXvu^Az&J_mz1ygH8^)*ghz@iF%D5cImCp@_
zLLnuV$L@c4O1vK!vKxJG=BZzb$z6`QoSc=po$X@RqFcFHgQib3#x}9t(Ju_sIg7fu
ztv578ZXNdcinJ9NJWuPm#KK8fjwgohQmZIzG*!dGG-6U
z9;I*hdhVF*W97HF8GBD-erLwjZYua3QtbMPqLWLj{J^6_4SvSn<_vUK2b+miuPh!N
z8^^tV6c)RYzCLrFX$@k0Q#ER;!z0
z&{n}UM8+W~
zqp(W(iFC$b!N|_Px`Bg5*oLX+^X5>djx|77BU)$0
ztzvV%OAV;k*LFliLi%NB#jVBkZoSmfpzsZi8)lKLGTkHlPYKBT&GoKW=mk;i
z9(5W32Lwk_{uns?0i24D`NNO9U_JUaBv&3L#Yl%4!u{G*pmdl+bY4Ar`eE
zU%%TfI~<}oe|tCyAJs@N=E0}$&^9&k@sw)vz7wIb%(|WW8$&Dkd&GPAP%$3lDElsoki}xvugECiN`C>po)SJvWPK
zCbH=BxU5YjM~~!fw}V|jfk4=IhY;`VDc%HO61$6GNZ#k!qge?+q@5hq`>h7TT+;u-
zmms&q1-jd@8o)=R1+kzjxY^OJ*L{BY#*COFslw1pvAug#lZL2J29U=xg;z40PKgg}
zjNW}8&UaU3dxE+(-Qr+o2ar3Y4g>~Hj(@ImXXJ>vJ3`oHb?vd8mU9f#OD**3H0kn}
z*>))o??E3eGug1TlGw!s$f;u4($t8$72zA%I@Ak!n%|;&MHf#$<~M;(QUL1r7N>X`
zf5qD_0k-`S{C=lVX?l)qL`LP#_o?)--g2@tBF_I7qf&EXHU8H?nVo_W(IR$1j@{++}BepgOwqIMOyfj8v|e|CMhjRamB
z@TK4I)jKl+Y2$UHR-9?&!J_!2Umi|eUB;4dkGMlw-v16fdQB7>Yesaj93OItwtwZd
zWyEj#VP=IIs3^x~bHCo2GyN;rBqqigyU-%yy#Fi_SNjS?2jv|p`7@E>lFBnZt0bv&
zBGgil@s9)e|9eUZLvs2WZolIGz%KgIwkG`b6E#MJ+S5za73VkSFYUtp3D2>TYy6{k
z-#q*m6lu`n=FO=B68?fjp%UbAf
z5XOlt|8)5H_P@a|ZZb(L9_L>_5psca&7t9Dsv}#+sV)B%*E0@p&w2hCpOpjvq^;ge
zo&9&2{ZA@Z_aC*UR`oH!UGt>37T;r2?MC(piD4QYS3`NQkQI-X=Z_?}{|mxxthpib
z>!$Vozae)VK5~D^ZK?T8{)YTZt?e!U2UZ7Ob4|jNe;&=-{d{o8qXgzOW8;JXTo+mK
z?(N|2+y8=KrzfA^xp7u?*WZwHjH}pRVm|>{hyR9%{H@luOaEP60AWVrZE(w_jKO@n
zL(>1vz_)|{A16<;%e{{Ui6X~iLm|w>#9aNxD21c)V+J&D8H;vu{y>w)QUU>AEh14%pDs?3llp?5qqq|^S
zG-FngqzaL(9{b^;B{L8m!$IY#)&dX=XLm$)36Q9JN88QEkd~FTbneS4Bok+^<
z@l$e8^)hh38C5^E$u!btRkTnMhX&0DS(x9F_jw_CF%P3|>jG@mylJ06CPu
zstf>#6T@J176x+Ak<0ucQvB?3QWnv%{Gc8q;)iMtvMa`mRH?u6t8^D#}#Qx2ls0FZmdlchIl&-jjn-bVRRz
z@w1Xgt=if~HvwB81pA){VDe%%@4TtkCM{;NHFKuDrzgUagjmY80z
zh#|+`JVcnm`l_7I_(*Nr=Q8$95jkm&n_>sfyS#nXNUMRRXX?Y%BXzA)@hYzgwDXm>
zi8W93E7S8IKe3#kT6mimg`MCv)!7j3BxobXhxXcP7CIf&cHZHxd1r>p-EyQv!)$bw
zuj#+_qC;uZ%^8QqRE#7?Y<3uF_g|cg9nwxDlDkH*>SRMSI5E>h0wM!2+I`0}$-)@w
zS#4ou#4s8x66;nbEcblVA9YkGNiWs@VF?xBgv&c1?ex_k!&rND?BX2FyEwQ(=_GbH(kLX-ch%d^2iF8GeWNUMRJ3bP
zrB6*Q=0|c|BjIaqJF+Mujagpu@^~O_C<#G6W-$d;CX9r!GdFEbx89Eq<{NDgGkc{a
z%?UH(E~B1YHLx}2T3!)NG*B&-HdNaA$#=>^*@#UButd>i+k~N%)Letd{L5ZX6gUO)^96M>_!W!Ap
z`<#(pjEW{mUYr~5PqnNv4w}c*Ahm3A*DJoZa(K-K@pU4>+aG{5Ulx}I?MCWunWIfj
zy$oF1S2M6dRzY?=IX+#Lg0`N(@-7?F%OPrc5+j`u^|#~+*9G{{!q%tJK2<#$zdYt>
zNS2W#(PR{GZC_}c<@*QxKo`HCOP&
z4kW!=b%3S1jKR8_Kcar!%W>6}Yt;_2EZMekI!b$|zGJ7;H?|iUoa*>L2z&2vw%fjc
zTz9KFP^0Ems}-w8C`E@vY$f^Fqc%x!mCzQ|iHL@V#ugP78A5}Y7ajH%#HQL>F;#-X5@%TrG)0$D;a5Fea_BhyorPZ
zPJ{Z9W)l?N2_%0*uN`082zD8D&bnJGojgbb{Ww4LuWOq
zR=g{LPP?wb97?`bvZF<=x@u&J9qBkPpJ;tpp(Fu_e1TmubmgF_dw%~{J4!$b1bzGiN7#RS+0}xdUk;5gT
zq4`<5e_6)=Y99PQht5Zc|Bfop{)ULauKDta8RjM?dvd3aN(&$bjJF$p3ujL_01gIB
zf(*?wxZRN=iIK-!1MIN_iA4a^_pv^vtmNl;PxfJ|4-4B-bL9M@O4`R7;cV_1`&9
z#>I-IdfsvyR@Yd3pIJZv1^B=3=KAMk>5#W(XtC%`={8+rl|jTB*!b|C(#&&63a&;{
zF~bBb^9fX=2i=fiZffDB==ISU5PEXXy0<614wis>3#NNbCRudf^cISlaN|0`@-zc#
zTA$4uQ
zE~B7D^QRj4@G9RK3G$n(?J$aNq>Hxd1`_+b>g;GGD*#z9wNT}cxpjsM*zzmWvu)fR
z7;e~)^@cE~k!hKy^yoQ>P5q+nAl5UtwqyCMl0*P-!F(LcF#v4s^Mh>vjW{qb5A-4H`PhU!ijbs?%&A81o
zlYV+lrN8(7eGzo?lyCXCq~;{CP%mAa4^MM?$9c=5zs)@c;2LG=wARJM
zno0yl=q`_r@`H>7HHKpl4O1~wH`hyBw$f9iTGe<(uTDwRep7-OJ
z;^+z=mUUcyN^H!gaeaoLJMLgB853_5WruJD5)g!KJJa-733$is{)lfs*~2t5(9|V_
z0`z`ie+z&_ifWE2PvUAP>V#hL5Fum{ziIBoNyQ1;06U_W<@M#G2f&kcX9-TCr4M8Y
zhYA*hlr5)36h9VM>iiP0?8Id)+cv43+R?>pifN{?w{O4r2&n(D;PoTIyb1;6=uGhW
z1XZl6FzEXSw;u_S@)_ootU7SA^1me!Zu7X^&@K+lx<@vC`Q#Mr2@JsIR*Na+mp?t;m#PkF$;}!5x
zE*t{cG=7cqy+M4-7JEF>`Q0>GO}Q7#D(ezU@2OcynV(nyPae@u50wqmbxyF{r`ILr
zSZSE2l+pCQdsM+yy|&h}R_hY=5}6x1=%IVa4eQrR6;~w_g+1kJ3<9mUKLp0RwkgaB-8XAen5yT
zwpa;yZC5pBl|Hl>F+qRCPA&`_IMl4Rw)>psWBA*KOUXXhUTu$N?$moU3oVZY#MFLl
z4Ek)cV~DGjeo5Qh4|!ID9`$8%)fd9;x4>~~mqEYZ{9P*7*eYb@Og@tuU^eWuXpzF~
z3Y=Y8HPmd@C~buN{^PfoRNC26akuS4)ApMmge8Xhfr*lb-eZ?0#!27OqV6XnIZI
z(WOUb(hCJqJSi5_dJPI~`m$jh!Bh4$Iig5En?yO~u34U*Zj{*MZ&Od~UR3}KYu&z-
zF%)y?nx{wZr<8@xB?L$Z)7~V%$2e%q<^Tgu
zGXM~0I}v{J{e5%fS=tKknZZlB!>)TKcPu^TN358YMkXn&+r25Fyt-p{eDqza+7S(#
zY%|qm`YFpPt@Eg9ywjrnK9N78V~!maEyZM$cG+TEY!&^}%cU$;r&o=X37z`=JDxgyfmr{TRZmSI`k1T
zq@PQtaV@@l=pi~h{|dT^j@{fbRNJsxu>eF|eIljvikzl|;~uOm`&5KpFhUUthDsxg
z8)99Y9OG!u#&TP`QANCGv?k#6xIf=pjyHen)#|&}5H_pQVtPzX2?wbvj=`|uH*-rO
z8kC+}p~2sE(p}Uxt&$y>RymW4VDX=a{ZL`eK;_QEA5w1eR@0xn^1C)ZAv)G0em-YN
zl+=CFi#@c2vR6h75Li%qsfi)O=n0d9sou+R@sC~*&q=|gnlr>vnV^TG=8Y=RY?*pYez6y&A)_9PJWLMDGYH6LIeDu7No;_{t7PDCsJ`A;`!hueD{b0@aduE
zO&JD(86T*W8E!WQM&mNQm(3?*Pgf()DQ%StAhrY7wkRD;(x4yrD(h7x!mmFTL
zPB~swRFYnN00|&Q_0!KtJMj?~xr`i7)M*Zag=^K*FPYT7^JcK)lB>p%%ZQOmR=>qk
z1pC|QGS{>G)|Vo~YAd&CokU!m?*1Z5l5l{>Ge?UB|A*#=<-Y_J<1}bLM4t*R9!dMu
zb?uzCAdC;E>m0Kix4nO*`(B3-1H!SJp=y^TcWl~qb|41!(jzm>`QS)y=qDPG5rFh
zOp2o106o${3QNV(Gu?08>3K8$MG+jTKAs#yq%;|~|
zY7R^&(6H`xzpq$I+5G_mZiJSHm82U^e|1)>7+Z+sh82Rh2;wuQnveAHD(&+3;f|(K
zP)07L0I36A^H@E}65{wpA_JvwF7v6thJ{EWJPlmAgeib77^Qd1kLmwx!-}
zjo&(-e%kB^IL0wCP&WZ)=PcJ2gVy^
zu(Js~yEI9;?iua8BcZX{9tu`5xkv+N9sLn~^X~R~sb&$CwuVwG(RR$+q$37HFs;@f}0$)c<7B7xV%fDAt?YK
z`fl6>aLh^@q=4oF4ZTh-rjjK3&QHnM*z`wcc4DtS`+jrSm#D$OoWO+QQxmETPb$7=
z@X4V1%IgbeYRQ98>@f0lxMw~ayx=pKSgSlcy$FB5NMIM|tXF}F({iggI
zC2MSPwGHo=c@yyD@^$d=iHO^-LpNhi)gyCNa;D;&@GJ%jq(wXy>9h`=O*GZlKR4Mq
ziT87m8nJr-(GjxNrqv%-i(#Ws+oO-+Z(__6-#A|W$^_UM;5bQLO_xvcL7o?iBC8V*
zqBiNsy3xHYKA~1ko|trHWV!#Ann&T_3*QD|cuH652=^`A3~<}BIBB=V1rx}~$eG#(
zqxbGwX@MVSPIElWWa1@s25g1;6RPXTV9yYr-j4b*_i+BvH6`=3;&9PW_YOTbCdNk8
zc*p_j_NB{d{Bwvxy>O#DJ5F>MczP~H+vVAchZqPnif2dE^7r#rXSE12J#?}_<$}eQ
z8|As>47MZjY8_r@2xd3FI=fgFh9c3a$h}7Ul^Uvk5W;ID<=&{z4k{47I>xiw21T)sZgb-C8A7Ig(fzI*c
zOS4S1IxeRwC&A%0_sS0`x1ju~Puvm!7*r9Em&pukYs{z8Zi~Or0UH7EO9{U0bEG3C
zVCbHcDitoNse$$A93@E2fRx*Ez}c8J^YWmY=R&EedZCkFxcctQ9d&G`-nT7XQATk`
zd9P`z&EdEUcu-C4`w36hy8a6X>x)7`=n*o}lydTGOlP%bX-0yB4H}E
zbD??!lZsfs2uepWGs<4MJa&pQ94OZvOWjLhR4*Hp%*;Ib6G|2ohYqd8KMuG
zM1|R;b7OBRxY@0f**Q}Y*KV0U+y;L)vp(AmPL}RRvL7h}ysH@MIR86{WwZHQ?*x@o
z)-X{z=rj#ySUhrZiNgGG46u9fIzA=)A()5v%?Z3FUYrto>31iD?Q=Qh25aiOUw5FpnJXX
z@uvsw8of5iUb}~P{~-@@&tGMyHs+uxA7vqUyQ=LUHHU-?6K}uM-mC0#5EGLqvKOiz
zKyDw(DXlx1%t@vwLy@J^q@m
zc$#Bx75n0mz(H#%i(bZ$6Pu=oq@F_ogQ7>f7DJN#(oc$^A71}#E!S`(DkN%h;j@GQ
zR9is6U
z$F@%#+NH}7HM+A$=r0fX+vZ-^twP?JHr6g8fkh=g%A6c+KbQ^
z!)BIma>a~n*Gv^7Mls1_C8hE0yr(Ei>NT@VAj>4jxWjN+%sIx2GQ&k!inZz=^_g^G
zU!T#r{o$|khRZruT&e;!%n2F_S^I`!&RjDxQvE%f#sE=rKr%de^-w~0qcl-undI*A
z@vWN#Hm*;Z&xV)8AZ#fKY_LcNEyi0fM=~>p`Jra|=)FV5vJp9>deC#{Bc<%huQ?ah
z)oK?d?)Ryn-oqIM0!l-dG>q}qYPpNbv#(_{+W|fA<8B%%IApd;;Ry;(+-d>QIJMin
zJiae00<>0Nzst#Vt;1l&_jZ}^J+^##R~p02Q+7zu>Y>jW*@R{xtAkeo;s+R{EJ4xP
zRl2sLinDOJ6B8uy(!b#B$>37H&aRZJ6Vrq1M-GI<1n{k5Zq$St-0|xvw?EMuy&1uy
zCb7?(PIt#u%=VfBZ{O4VhSew#ha-o(W71VV#?2c6DTN`y3r2tOQOeO=_-8L;JU
zj(kS1v2<2C=Iir_qsi=MCy
zM?B68GEo*b($wL+>V?QG8%BM-?Iy7MA4&`Y0+5Dr
z1<;lpRsrGBKT$7av)pT5aq0iDB?x>xFH#hWbZDr_0gaLO5$nf(PEBuwRBcLb8OX-(
zPm2ako*X4eW<}RJ)oUy?F&8czZAlFF&nG&)<&JZa5%(t9JyuJa0nsc`q}%G;O!w;S
zpT;=(=f{*>O7^nDN^lLt&$hgTDzI1Km)WHyO=nDp#fH|#H!_ADIRf{~{sWyzcLvoU
zlFJ&%6;_6wWzW-4gcuw7({9?GJG}k7Y|ToDl{^z=4^jO9RPjA>pouJ@K0BERsON>%
zT7&NHG}9@97M@^_%}a-4ywx~N`xSB~MX=5ttj@7N+#oN_>GNJmTD0FhCM21p;ii3w
z@|lQn6WKvM?FB4`?2qYBffR|8HT11wKqAQU>vyA;BL~!X;FpR3VpM}jckJp@z)797
z-?IkdtgtGT@(u}F-?R1Yq+I8C1%WSBhpg}Mzigh+Gw?%hsA3EC;iAEX2f?>b+WuU<
z4d-HdVoMsr;^pW6gnm2Kadu5p=(ZIrS16{4ngN^Z*y9g)?~_|53TuSYS8v*n7*jun
zA1$1_;U%W20VHvTEDLrriwkrk7R<{>Ag>3T?-)=)r>IaDl~j)=Lz&dt-CTF&F<<%I
z+b`ongp7hj0pO&A-3aheiNfpoNpx1nNbMst3N7^#qU@Y1CU}|4OZ4q>ET2$LUD*6h
zHbxMO6m~jPOcX6uB#h3v#G`%*>_c@Nqf;dmtv8V9hjRI2N!O(!-N3(=<1JEK
z?7M3xtf4vmo?dzbos^TplGVeu^3^F_?Tx`_Yus-7WuCqz(LNvPJV=d{)t`sMw#IBu
zcT^|Rn}Im>M5ADTi}6;z5yJ!!{^J6z`yvS9(5;t_
zSLO%yQJzU}j(Tj}KQY|R9U5bA1`i_(65rU|;&_cxf|{qeXnHfk{$pX;H~9RIeDa)R
z{7F+acoMut$5ykbJ`>SN%R){Vg|j7!pB$ML#Da8@|98ykYgS3edInZQXjQjlG0cD;
z67Ww++d6lvxiQ!Ghyg5Ht5$%AfEFCdBL1|kH;pF%Jg>rc#1TuZ(K$aqIXqsBP>jy>
z%Vz|79DOIm8W0UgG`EyaUytPn27^?a?*l(Y70d2hVk*@YkSlmW6!FXwl}|;Au?#d6
zb`aI6{KgCyWm)5+$p!9LL%WEIm&7n9KOpbCX2-C$G>QdtV65@@t$B1$AxH%
z3G*Wg_wo@Y-K*d@cX;~&-fw?5*#&n0Gb649jb}F}{Cr`-r}`5JeCgWU_S4q+H4sxX
zy~rwK9UXH_pdaDNy9;l1GqBZ+1LZ@YGB^6c4YcM>zhXQs(^YcJL}mflIt^WaXZlOv
z!R?((5<9$K0^4^d1r+sIEusrK>!~VWkEMDv9x_R1)EyjfIAWwQpm$ASD=0$$yTt%t
zZ=dCD>=3wPIE#NyUV8J>3zH@?ypw|=D@E!#rdG|Bfn_nHji9EF3O6Ij6$CeuvO&cr
zTvm@QlMD0soJ!aHl{Auh>dZO-+^RGdenAW-qo*
zaxRv>ZIwZxCep?GY}F$nAIRI&smL9%V{LWqhQ%>a-I)Ym4~icblPxE&OXG`aVPf;Q
zV1`U{GB~0nC#R=!WqvYhU6#th@0-71%__HLC0`RXP+Ls?r19GRd~PR)jvJ-BMl|%>
z&Zt%&ovtGaI!7uqDOUY0ZZkpAUo}ZLXm2rWk;*f9?15MUl|+PZh^rNEhaOX|(|Qj_fE6^Ko^N2~GqBjoVr)*nv<=rP&9AuJbh%Gb@W7l*f2e|P_>RDUkTY&Ar?
z)R*g$;HB+(em0#gHJ*ow0PEA4Rqmy2gLmSGKqSgRF`6-_y?nWU-g-yVyt%yaSP`|1
zne%99xSbb3Tn%L5t?3m6;bX79SNLVu;Ixid0qlyoThPYFPT?Xh|jm~y@~
z{QXJ%hyOdEf7AH409RnxFrb~G7n}Y=v2~-P!KgEYs6c2^cQ!nkV@%8aB`|n|rKajc
z3rVMl(QFywEZq&B765?)?g}>sS~DVIAGG|G_gN4$cC31*FrrjH6pCA*a_8;k{Oyl^
zZQ{h4cXf{z_}2XrP>>;2x{bY>4jqDF5eic-&kPkkW}$pi&5gAKtAcI~$m)op5i=t&
zdI7(@s+HMR+1Xi)NpC8{)2C-ndcdt(8wv>p~6`sB^699E{Fd~t0^kr&WtZW%1
z(v87S_|q^O8BgBN38qu_6*^`>s-kZW8|;xSsWaRBak6zId*wA`XY%?BqL6iidKbDg
z%P!4IKC}9m+1#M_CUAd_A3Hheobe|F<_OCynWn=uTR&3enG4j!)9my9Tv!zBLNM(X(
z>#e_;BLCI3TTDJE>tjV-j$m3<&)*@b1oqda@<^ihd74M=*lJdZw<-0U5Q=ngX@vr@
zTtHa#B?7{W*ix7XtrR$r?qQcGV9=NQsK`^(*{L74H7SxmP`MAgDKzg1?5uJ)Y!F~M6X4&0w6sBP4bQ)CbmHA5m
zMAyhF_VIOs8#QfN8e6+`PU$}B&Eeqa*af>h5WDNP!rdFjb@BX#T@!Q#xa2qf|
zt!CHyGjdkiW~aNU+>a%9-sCe!-mqW4)jir`2%`~lPajc}-OJZ3-(BzLeQ0zUn!a3a
z+li~W?2(3eQ_Vj;MO$v3r)Js;XXXr$^i2j5#3V_g;X}#F1er#zFgiYAsP718T{H2_
zR@%Xm3X2K`hFLWcpJ%8}B|+ftlb55@f*+Wu#jsq3G(*EtJ;{r7vI@kv9=WqN-+Y5#il^Jk4xOR0|YfQqwWMPr=G6lmRg
z>pgJcw;=$&Twb74nvowt56O^pL%Ib9dR?=h^LGX*C=$At3_zvWA2Scy4TY`wcbFv*1i5TJ5Gzr3&T5op%#8S>0l
zxY@OAmW)>*YK1al(J^93V}~u06&?SSzLm8}rw0w$O
zj7FH3eAy-lUnnw>o&O5i3Ky|4+x>#NjB=p2=Bm)ogSJX7B%1+4$Cvf`9;(_6HkXUv
z$c8a;wNrZ-U2zlnvkh)zn}mh2U3?|B4BU4`=$A1>vwl4T1k2Tnn#L_`YCNyFgj>+c
zkZLkokT%yg+R3#~aj^!5hUz^kd-$?3CCn_)0$w6?EoI(_DxXmA{i^!wuE9RpS9-5f
zjy`=!on2mbwJHO8@l!c&bbbn&-UOn@Q(E-6m-tTL^CH{V&(ax;yvduNrDTPv)2i{#Wqm^~40st8Q+>0D%TBFfv|#^|M_uL}+tU$*)l
z1f#B~DZ6*+@C6V(-#gJgZ~gZBhYhZ~kOwQo>m(U01`iITSo)xG3x0r-uXa~n=$Cqt
zKLMkdqaCHwjH?ZzC6adgaE`vZz*~zB$WFAj%k+SgNcvvEQV0@#H1`SI_l}+PZpLU?16nKn>^5>$r#gcoIZfWTi$<
z<57+)^cslSCwaxt>#82jA>#RV+<56kP-Th^x`EswKEN*py^kOlQq)w7%AS%kOOlmD
zwK<(_%aMRO?WIlN!{GkI-cz5
z8+><_N>KBx9?TGMFvjj5`RfMoul7H0Vy)N;z8vQBaX#jIQKliJIdv}g)Kkf$s>zjw
z5*4pbFOIBAbN%}}Sd9*1BJZKL^kDBdaNDiuB(pQsfecaq8w*f0Lk!gZ+1aZ*&p~H9
zt3CSG=|K~Tq1@$Fy`+6lE9j|u3F$UC`a}NG)9sRA_}uK
z`Ub_%SyzJ0t3D1qnaKQ(U+`nlKQ9$k#QbmIJ^h
zjkG`;Z+tdu?QHLA(_J?F=|SoQSgJGPwN?(QuuQMe{H&*HF)qvE_hoIt8&5}B847g%
zN%edmyyt1IXmN~9xBk0v6OG8=$+El3P)|3F^5oh+JArung~Z0ozxQ4@U->3oaH`{^
z8apLPJ;xap6%sH`XJ9HZl(tRzYCk?wwE_a0&`ci0A?qYgyUE`ft*ko8>D9?)rbOF3
zA~&Tp(Q6Ujm)LjyHL!ozQW$vpOThliWyn8s_IvLL#2e1PDuh%AWy*q!7Nm^!E7|2d
z?!mUEC5J0>`6Uj`MG*f=zz9nxS)UErY_>D>N!ybc@TvqIM)oCdqon5vQlw}I5`yenPY?w8<;P?TRS?F;Z2+gaNU__>G20&t;}E$lzKi-Oe`J+
zVq^tQkv*eBrvt3$yf=)J++2!&&)!~4g!z1fAw3h}tw6rJo%C4a#QcX{95-Bn+;B{O
zplg0=QHx?EAi3bKwor+#3LnA)?x)6Fi*j`aHrC1K?9Z^%UcUd~FNVy-h}!U!&CA;$
zHgqa=Ri*#2yR4$%eC)-uWwyjEH(m2d%K7u+FjFV?tg?Ai_sAT25-4^tCBC_RAmqqi
zV8nF@k_tss5ybdc5Gu6>Se`xbwm@}oT|UEB4+X;*SkU<*uk;+2Jsg9%(t+t7rF$3Y
z);q0Wu?5n$Ux_<1YKMObh;ExJQiRPLOx!vHV<~~bS!&{Xy#<2UFKoa-a|Rcm$T@1a
zWpB0=w_|t@?l59f?}Tcw)To#H3_w0A1x=H(c{)eyZi|$1Xdi2FJl+#V)qs{SA{H+F}E@8iYL-oIy7=tt~MI
zuxb(|vsnIB2;Z<7YT>N|VrWtsp^Y4Y&Ue}Dqx&9=yuHD`3?CfUHLhgvt)rHk2x4(T
z3sTGroTOg@IHU`?$H_V@L!Si_3qUQ%bg=PjXB_LwO;}+
z9r(G(3f2v^PjYg%4}`mitPd3kztCeoUK>&eu|u0C|Hjm0~Bav+P!Hxt@aICWLNusTVkyrTUYUcczYHMh1`jN)yh
zn!O1=ix~PBgz_llRw1dhh!LwxY3JVOzKd#zq+|rQnI3jEbF6kmq3HeroRKmoI+)d{
z1Jd|Hmo-Yim!6vN81cAz!+}*Vz1N}G4|QG9K|Oi?RN@<#c*!w?BTt?dqjKBn@K@s;
z2fR63yfaUjip?XaA26K%x-0r&NYB}175|U$;5Eb7(mJ}j^reVmct7yb2{KC-;
zOSNwnZ6RB(o7&DWo~L6j7Z*}&mGeQ!o}Il!@$YYvALWgurx&Psx;)-gb}zQbj7u(X
z!O`)3!~OtJ3+37;TiOuI%dBoVGC5D}3M6y5SUQz^-In0N2uq=V*pvRY0d^u=F7Zvn
zF)^sPgu8==(x795Q4Ci81ag4_WcWv@5yF4+ugQGLqb)c0hgXl+N_L!-bm-0mrWt=64FTI_A>daA
z0GxA!^BaB^`}gmr6iES{~q+kc$>T8m9JdzN~QL5kCz5@6YkPm6>Ig|07;;SU})%;o4?Af?u@%FXG#s18EYsY%o?UDvYPS
zBOXkcq4e!T4iY7yZ?zBO05e;)BBGj3-(d|!uw#`jM@)_j`I!hzNOK#ma
zuR7HaphIq+%<))t>ucg_*3?5)G#u1a3mrtsea34}Ea3@G&KJxYqUQ%fXaoH9-AC)h
z9tTV7>jnzsRjK)^%qeuEn88hZ`&VhHl;8sC`GO&xb8|K{qLXpY8}A~0=8K;D!T>%p
zSTU|T-F%@hr?QDv9KkHZF#?|BW|YT)h|v*5eK~Mn-9A}Noto;a^>vSekgx9;q2J0i
zlTZV;H?|X5+pFA~@d1VAZSKn5f3l7VVc&>VC_&)jzHz;{HQrJn4We^844v&{vV$0B1T^+LeLaABmPs(fd
zPEK~bRu3b*hioc;_Z}2_q`jgEOhty;6|OE^?v_D<=vyT&qkM}H
zLuw_shvoQ0urrn|~v8yy^XL~h2-{Q#v4dgoDetM|s>&=V3_jmV3TO|h%r5DJJjTjvYRrJ78
z+pgcE)_9A&9x6DcJ!5l)z1a|=CI;M}Yd|mwRauk&+
z4dYH5lxV$szGd?0ZCGx2-qV7S?yu$<`rSEs7|#Oz29Spk7R;-@?=IAv&_GXQRT{MT
ztxc~ZiwR=r#u$m4gm%Vha^eB_<@EYbI$$UtqE;{Z=iNv9wccPxK}LwRlQPO#&l!jd^P5LGl!un*5YhBn>(dW~#uY_B6STj1On0UPDHBbU
z?)<0s|4;P(DFyW%`>kIB{<|*<69JY<*ASL|$3m(``k~c_`yM;ooZ%{CPO}hpq_aGN
zJ!(Ax@44-|mSRcs;znlA!(e52sW0{Y)?`-)iI)+JwkbdE^xr?x>*H9Fhj6qOs>SQq
ziy0g9bC=)gml?A64iy}=?oKz-gLRIczQ+$P>EAl<1nzneNVuf_O#h(Q79S5k#%8bq
zF)3WL2$?+FYATZ{4w%xj)Lh|I-z6ki-}@EdNh$>Wc5?2+vnES_{dSw0?eGT5=mOxbCwkgWw
zxh*LyLiq%^x!6^PQ@W(gGgC{ISU-zN8irp0wTEGkUDZDRPEBomY^dk3x8hv?0+MVo
z0w5~2VD}oWPIVBgLkT0FY{q%T)LcaO6u2vP^2d2G^qru)Y0+m|lfA}gpKp`U$(YI2
z$)ES-_nR&bwXW1jbuFYju^5VOT$BuzpW&*U677D17Wnk$^R+Qf2mHuK6A9lbU-ASz
zhTI-rpT-4rI$#MUU*ByZUT$C6Vu_Dkjq9Y~luHy&7dVixHi}
z`ZA%Ojo1;t-b8w#CrO-AW9Wl`Ms()rur-F`rZoN#+CEEkGdrSQ?!W2J7p0*X<$^YTHGTpX))(Px<6ckCQqw-zNb=2cLj
zTGf4PJ3s`AL8nJsJ*zpx5vJ!%mh*s*w=e2m>~MICr&3c32Ad7DTvCyAO~98erdeRn
zk4wTC-|R8!+!lMRL2;e_jOfh-Hod!0yGZ*D4!4mjayE(`(%H$%>E=yhjH(iPjrXba
z|Fo3;-zBQQ`{uv<$NV?M(s_R;r&)hn`OeK%+DTVI^RFhjF~#vO8u??VFBi>6AMHA+
zGXOZK53PLAS%4WRR_S-*Yucs;{a;9yyi{-{uEP1(bA8xCT8))T;YAuf=prH>jM
zOrXCe6}V0K6PdlqM6c9dEH$h40b@8)L7l?j~_MBJ~0mpg~qbyZm+8-JLuT=L6>*(84
ze#1#V9G`WkS3DV9yKW1@^C1#!?EdJBfHv&`lo_>D5W;;=2zj5QYxH%~YYcGr8gqPp
z=MBbM=Chb6O93NXJYA^IWv~0&Hi{x=qEBAkuW2l$M_|4zZu5h$V*Bx!47gsH&yO1DdIevhnjRA~E%M9t=Zny5`giAWUt5Z4
z_(MM`h}G`Jotx~A$<2l%9yPfz|G-;Mezi?uN0(%UWD59Z?Md>euv`%_iA91fm^IOK
zkY^}=NClsde}*N!>IDZo>&`i;N$9L2++PJ{VPtiA*F`TPK(Imh&3C2!ojT_u`4z05YFUsO~_|LC(p^65+e56
zTEE9NF0&TbdgiPPb)hsCd77X)UY4BNjUBBh5?SRNxelNCyJ`JjegBILs!cq1Ud1i3
z1hBb+0F@20+=*k>np`O2d208)(gV1l?s-!1=X4zpW;b<=EiCLHCefPA3QM%v^)T%K
z4h+*L7X?_QMwS6cdc7+FzC77YQj1YHM~fH?mkH6Rsize=>o(;u#r$$l9M^irsXNbx
zx<~4}f=bLis^#9T~;APvVFW<-91`I8Z@
zBG6JLa95b7Y-^L_J5uvaSZca2y49RA#pdr&I~tEFo`(7#5+!YO<;K`+?8!ws2;?bd
z#4q4J*@gE|0OxpC6gpNuUEMcD@Jfl@@-ERrO)iAoai}?gXQ;W;T?&Z~;N)0JXu+9A
z2b*Vmq{T(rJG+!=)Liq<31Kg$JsF4n$HlP%w-T3k&~Fq-rI3}0{gIq{kFh{D$88?5
zsr*Nv%`QMmsH7y3q_*cWPh*Vi3E*c!~H=>yyX#
zgU69(cSi9W>!z!3A&|*tB~aKQnMVtB5;+K&uFuFX)W=dk?C;DA8)c6E
zI2iCka{DX=rWa}HU_4Z5>ub0&Vm%)j%2yb%HjEK{y2XX-lw5UYWWW`yrugd{9-a|$
zV^7P5zkX1{>HAk!9#0lpcFrS~mp2~Mjl~NdM-D44aQp1KgFtjosZj>sgv$0Ye@94t
zS?u;WBOjp@VuVmS!|W-%4PBqF-~8Q$BgGmSG5=x;YoG6|7hb&%)|>))hfP&H?iK(&
zy<0u9mXI1tNJbC1*T>OI{?h>d`$@kmZ5yOiR6K4AbB)Av?9cvVzPr-|CGGF38+axQXQ4iu2Ae(4Kq7#>jqE!CfbBEe!~%~PImB$J5t
z|1II!@IS23%Y3!PV$jLHFAgLMX-c}=;4vvwiks669ObOZofw`g@;C=Jwm$0I-B5ye
zTq)#BnQK)=tm|A9LT5uz{16<64ebBAk88j63g$ejl=bfsy&82s
zm!49P5>I5m&*E91ymBBl@qTXS*9P*<*~MZ9e(u-|A}XZE0pLC-3Yat(v{#G9Q^gK95w4H0&;SFCTs;
zBW)i{&9cH!^5jse@5(AD%%_qTy4ykN?liT=o~Ylp+^z!%IlaW
zyqfhllcD^WJlSF@wPo{wGrY#gZKr|v&dT3{FU_Kw%?;o#F9kjS_JzCv@i5KfBCbA`
ziYk6AnQ+(}$NCVeY$Fm`ACSAB?M-|FBDqu)0Nz5
z$PV$^4iR358Mtj(8@xnBY)y#$f9$p-Bk{f{v2Vi-8m%
z6bVfdib6mH)KP~{z<>!71SNq$Dg+D&2o`!(O6XXS5}E>nBBS#=TbbW;uIIU~_q^x4
z*L%(%&l&zmfQxMQzW2JBrovOabC#lg}ieB
z9s2Z%Sv)2Y7wiQH6io&DI2YtUv9p?SEX}bmvGdZ+a~3yAm_yxp6O*x<2tR-Ks*dL#
zETRu9J#flojUQnqu9j_}(QgoPeAJ6JJagxqP#7F}RP>hiob@2p{O|`>mb*GW(l$sx
zPfE?+DhG)itrrU#=|G8%NRhjnE>1^j3%X|?y1=MA$8!5L`f-@%{;APKw6zg0O_@cZ
zfJ4q^FiFOQ)GG4JniW%|)KUYW9#>sLl%CZo;Wj^9us{9~*d7H9x%xu7;^q#;zl46l
zC}PU2M?0pSS#wZ4k3BhFbGkR(Z+j~k5NyfZ^<3HRcC`e(Nt2xr*p
zLqDzk^zz@6{68b44t-*sJZTmm{P+tGdlk~w5dHk=7IF3vK(noU{KwOMKi+Ts0MyF-
z&q1~S%|3S4mWC=W3bMPNYmy`OzsLpt@Bgu#+P!#CZHot>Shl+VpJDnR5v6w;U#L7i
zd!T>NvCZ@UK34j_zxn)U()MAcUiDBIDW~Fk?(X0J|LuML3j*A;d2@Gu{6z8ZJM~$-
zp1l+r|Ia_y5BFR+&co2RRcjAB1T8Fmi`_Mt}J|k)hk3
zKLEO0r+N7t0IvS=j>9hIPJG8!e}6y2Osfcr0-)7D-o=0XIe@JG=VSj2$v;-|&&2ue
z9R6_%|LcCza;7+CO-A)gomlxkKe96E+bNA#|CN+$K;sMRplR>iqrIeVw@Et1LYAr$zfdUKzrTp#7~#B
z#RKh&s5@^9T?bmmy(>D!cj!tFmL1Ot`70mDLgN
zNz%5U*G1fLJDN)6$4|E>k;080ea66vJz8#$kc8_yP)~1j%zMb*9e>oeWmbM1Sc8NI
z5{Ae0uO6qG?U!?YZY)tp9)Hw@FcCi@f^3SF@j5Q4v50}|*9Aygh`Yv^A@OUi`t?z(F
zzo{}CClXw0sbEdEqRicg>9J;3?67HapQ%)MWuRB-m4^ZiUazuIf-usx+14_lZNdq~
zCSga1ba>pc>!RN;UzyI>=})~}b%Kto{}3?(u!51u@%WMgFg2#6gp}2&1z#kkMb`iv
zLD&kd@K!7H3*#oXJTiB_!uKPi_Oe>Rzr^yQg$O9@P0YjaeKYrReIxtSz~I7XlUXO=
zVP$dWVOAr?ONesC3n!LT4AXq?UIY3dJkB69QGT3=7iTnK8O~n~r_oJ=0=eW?K3A}*
z955DOZe(@_DGS=>vxoWugC-)>*mK^Z31v!wb)6PyAQIF{Gtv3%gZpXxr?xLAoAqP#
z7p0Z1%ayUNY1wO&y0R@@Vcs)zA5MY?G2Ymo1;LQvp;3sMq5nuI{&P77T70qPd6X88`YCPY?Z^x0lC?`o#`+p|3%KUV1aATe@w~9iHih>Gc-2E$Gzm
zk}(sR%7qooX;UvJz!B$#G4fJ!+lTWbgQCnU%oJV?vh#y@D4Om-=tR_TT0nJe7@B!u
zxnyYa?w(_o+aJa8?IiIuBP<<7uIuDX%tNx<9t}rVn6tCHvU%3oG$WZBjN-?jj;1=a
zaezbB%8i%LI!jH@7d0_OV~OX<52DVC?Ji!rT6e?eUt*l=W8XT(q_V79aw3LF!0b7?xzjf;
z=w_~F{$Y=lzj*Q^ow?B^sP^jOHmQjk`MEKBU%OIasKDZ=cN(FVpw!@{<>q4s%k(8@
z-bmh<^tTycE|cYm(NT4uJlZIuX#q98rJgKc*Fke&CkGo`Zz$EhxM}g5a4q{l8L&oY
zt#s!hf_Otc0ERTrvetUP!0&^!k=&)~yy9k7kKN8Z$e8(%g|gsfFu=A_RT1i0YI54u
zqp$BdHryTw{He^kn80J$CBd;0H?1IReP-$cJJW7_gnMTIGBPBiqYAU;;k8sk_s$S{|81
z6(Q=i1(}r>G5V)oDu_SgZVar=FPbwMi1!5tTquM_Wtfg9S-10J
zY_*zaYHk@nEY07mO3*hiJ}80OkMRTIlvt*FZWN=y(^0Fni;>x3=$QAE(PVt=
z)gs8SlWN;}PN>8&t1s_jpJzp;kOpn4)2JuU5s`)}Ax*T_pCHBNrLlk`i?d<1YV7>%IN>Na>Wd7BVf`LO1R4P-RM5rT(vB
z9#l1SxdKecExAO$hj;Hh?Cn=)ePIq3!n>hD
z+DTpX((vL)*4VYQYEY8y2IKa5&^C9XXi!V#pw7?gS%Lv>ZXhhug9wXm8H0QX*hbq%
zjp$8&cUNWC-KyP1J&gf)MM+&y$If|WAH-~oHY`G`YSA8I?2ZtfaGfuDE_$8Z(#H1M
zt!y5G5maco8XO_Xzlpukc=cdNuWYLgKU*X4Af
zi3#V?Uyw>um_?@?Rd*A;%=1sR`l_hMMw^K~xtT=N7r;r?RIiM*F|QxiiPoe>x9Lo(
z)>d)a^oQ5TFW*)=CpkLG-sbn6^x0Mn?4WWwwj7wgs^|4fS*a&6^+|+Dh8>%_*2wXq9UsyHzq}za?
z3cOTpyFl;T6`MLNnkr#3ID`!1%dWwR)btlos2VujwGfVeC4L?>u;~X=R$tfeLp%Rs
zu4Q34>4$b1tC9X3;e`e-+K(7>Z}al(6&2?($$bL`#z@37r9~`mg4MJ5#O_OlKI)U8
z*X8rDc%Q^}GgrIylq1Dl24R$(P^n){s<7bTvwAS9T3Mnh#FI0tbQ__5N60B^;uY$A
zPQN2qOG<%DJV>Y`A+@KFBa;}f--dt60I*J*i>k5lYBHX>HD98lg0}?ki7v*J)V`-jF?^a#&|KI1lk9
z-`=L9mhASHlu?LKEQk)q$3d>Pwbm^z{N|7Hk!j*
zxCeW`%KN7>3g2^i*{Uaz)(89Sfx^-cDE|KF0H<4fX*~D2e`Lx3pMJAzb_7({@_HvA
zurf0JmHl4xzwFV1!j2UR%3Apnu0Qp~TmF*0&+Ro@)@bdcfi*w5r3S*U@u=|_sL-iW
zyuHo7(MY$Z_DmA?(Wg7u6aCB6q&2>yv-56+&wRhg9JC_x)$V%~WK};wY%fIP7Y*gz
zN14Zh-_ZRcjCazlS^$}3n!8LJmPIhJ0{d*6CtTh^etD*whVHdm@+@$HW~fxDsJXyb3L4a}fB6mxB|ig~;n-zUAEg
zPmia5VV84cBxxM7oO(e)E~&na9R-Wf_{8h~KQ(uL{;prTz3scK(NE{acDjBm(Ca9e
z^#Av>lU9L+KQ5)eFOgfMuv^~9Fa$!|0MM|+(jASD^!QLT
z;A6KP9qJlw#Ju@zxdq#Lb(^2x=3rVs)o=+6?8`_%UApP
z{P8a<@evHtT31m*mLo{R+%e8Oc>ph^RPE<#*xOUpnXj~?YWKe0_|yCPG+Myx6?The
zo;u6yY{Z6toaaJu+4Y}fN(#}wtkYRaD)9!mU0>0P+>)6+jlt4a=tx>ggaFBRPpN*2
z3ycn^gzvd6HmJX}tC3$j?#<%is%%r9Xa;(Q_-P0syO?4`S{bmbXa~(;8Hk7p
zR&pxEk@1(>jPNRD5|A+a@zu%k%s0cg{E7OWxPoKLpL_+sDy`J_w+JI>HLLnP)?OWK
z09MdK87ABoCn4Lbg`&G8M0#%89#H-!YB)cChd*ucv^>7zJEdLXJ;dNN4{y+uy~SC-1-!Fl~rb(Vf-e
zdqR=Hjp(%`Dl?UC8yw?JtN=>92N1PP2(@i=Ml~iHYF&T5XAKIq4iS{$S*)dpBsPe(
zl-i&=wxra_vqtw7>B#fHNr{PbvodTD+a2HI|
zgcRNHT?hn8axriVn&C!9;Wns)0wfZs=qJML#{xDTZ*T98q8{58jB!}B+!w!(pU%M6
zm)>b>g><7v>X358iaO(_YGODjUna4zixP5Hu%292Z{K&M#$&NM46W~$^2T@&=kW^J
z&?YwI-)j~O4larch-T!wiQb|G5o`=uPKDb8v=G9TlG#x!+;1l|f0CDTha-DfIF=@n
zWcpD}3dyZ4UKP~jJoouEjN$*&O=d4On0f3$C=43^3J|TfkOF3GV{vkn7v56=DA1cmy!PXiaw`^B@)Wl1zj?m*tyBI|DV1g$L92!O
zg%ImWpcX*`q;_=SoAk@pa%hXk@N^lsja@
zuF1}dMSv_vR7EWk=AEYI(pIqR$eG(>C40ki$Do%M%4XZYov2T@BwQmJB@B0%g%Ox*
zk(&0?R)K=D_|laAaE-1}pVqYI2cQ|+l$l?PZLJ@0dY&b0{t
z+QXe0e~}OCj=%}1f+5_d!K+X8Re|dV=1;(hq1jp>3hv5YC)BGiE`Qyc`-Nj+JnI(6
zTNHxyhYKXFQ;{y6$x=3PnH>&Rq-(g5~Z(SGJ<|rG6pT^Bf<}
z!SRqNRn@B!-T0+J$1%x26nBnxQ5An%XN8|&g__S9;M_@4X<$>8(wa!Uz6csny@94<
zJ;pD?j_Kzz;@2T?^#P`>DvUkagw}12M0zwxzv{>txpvD}EI3=#9bk1lJzMKy5hu!n
z50-PQhCR7K`9ki&Y`90WXkHFI+vpEBj942uu;xSC-O^C3nnHozFU;In;)
zKcri(n8}$v=IScTR<8gH)FR?1^_7w{aAWgh0glY-us*@dO_d8B{-rD%ciUf7HHNDP
zP8x-ezAj2-&u~A{@(yj=op@o%;ui$+m<6=Im9u=GBo#@QikAl}PNjel;SAym!>-I3
zxfG<7w_5x7NJxe_QZy|}vvELG`E(~?I5~~%BBB@{5|^J~b$Ce0~8lm<@0D>OQ
zU#bWNy8J&NUajkYrCosO=D(F})K_wqr_qpJAp~iDikX+3)R(&pG_Yz3(6w&baID=z
zR|9=(5E1N6F21&Z_uHFH!6m%8Nli}#tEoXQpCU?mBVbM%v+6+sNUNWW?lQoc&jyz|
z%~X**X!r|;&tBS}Is%8~ZD(6*bf57~M>IOakJB*=cRl1+eBuP-p{d$U^i*x!(d
z_9-lPwd${%)-^STAahu0iV%sAWEN9@m4j#T-KN7G$vWk$)_*)xT5}Ps&J-12Rnuh#
z_)#E8ul&wB>J-H6T*
zR&t({lxtII&~GZ9D5AcSZbeEEH6QF7uVu6=Mg0oN)es4X%js2!7y?ZD``xY&2xgi>
z*3Fv<{K(+9vBoh)L^uX)Huxfr#pP6cCB72tTmH-W=uv{T+|$Bl{lq7-`RmA-Y@C!J
zJhwxLruSCCmf^<6gBrzO@An3S-$tI4ZC;*VG~3_b><&BWu0@Z7m*L6X2+TCBzAQ3A
z%ho|ver
z;w1X(WPh+lm`>^t(#6*3PC4QfB;yydoKi9uYvGa1O3sL&DV75q>z!$3v15UV&&>ae
z_Tm3mA58xJ{PAzHvKV@=u==IjkJs;5-4m}{ERK9RGjsh{?9fJJUi^XOvyoxG;oMnJ
z*~0q${-z!NJ5D{jB;n$K%gv6sMTe}I*_&k)+~8y(<^}EN7WBTcMP@t2B4bmzCYg@R
zi{kG#j0qx4Y(lfRY9#_&U8mojLOJcm0-8St8c%);+85C9n_{c{$
z27vsj`4etjQ$1Qb-X66rOq|ty^0|5odas|Bkm>d)c2Gm>ODzDzLvjRV*Tui>ABk*y
zuo*6|lX(6dx?ZDIe`itd%YG{ssb#39wsqs~L~=5#4vqDcd;|Jqkv{M;*eTujrLo2>
z8EVW7T9r*Yu&p2$^{27GWYHqlx=7aSwBvU2=xjS2e8$?Da4h%V$L!VRlD87c>!v`L6jO%8zyK=L1kY(`=a+?NA^;HGe@7&w1VHtt-Tm{_~
zO+X)p+YVmlzX-X|?8qkbqBga2>N(;fiNH)h!lU)wtP5l2u{B;2d<0Zflxnpm==M$!
z0sj?z;6IFH`*+6queZOq+5Y@gY~SBXl5KxKot)okW={w`)m9ZeOR=()nI^Xl4+|sj
zHK;q0f1bd;Kg01k{35R-6m$-OLcAFkm`lBFPi-m-v8(I{G+j_MK+LdK7wzuaa0xH7ZenE(w4fJJOfR*nP17ij4A8rcu;D)I*5eIfE*$G6!qmj|9Xp
zP#x1yv=2@#VsW|mx7X`wQ@}yxYQF%Ks@gHVUI1t7;3MDkIyJb)LqBWJ)D`@Lt
zZ#O9ELn0CGNWF?rqTh_Wv0udb@rm`tAZJ_pXySmic7+8=s|`Rt%gN07_&tO|E|4@ylp601s=?#T0s+@Bmw#)B^k7y(*p%#xd-p2<8k
zC!=F#rdCZ#rVw4Uzw)F!o|RgXkq`O$FEMXWtvLVkVdHw`%0frioqeV_{n%Qiv*OP`
z*|QVWU=h5bkccIxKX9*u`l^#hZsEE&_4=!5Xi{Wvof+cK>*9Z%y`8JGWdpqSqSPhT
zyk85SFW^zRy~aF%h0pKjq!3_TYmsD7>2yb$KVr@+DQ8F1!dMX%ZjCu@R#F&Z*e~D<
zGVtC*HSTdVoeE$ppyZBZrCY;yvnurUWcZ%=1IZc=uw@>h-*lTS*!71~q=6-9Hr3k1
zuPj%O02N`1zX@YQos}`AqUF)e`M<8{C<(#LC2}S?_^MQ@7}tgTohx7
zzqdv_MyXOKd)Gem#BLuSkW$WbQGSMq#Wr+$iSSlk?
z3Dz;^OYF7Od+C`OYx=$d%y!E_Kv!u;3B)ZkE`$}We+(WxcC+&jyN+FUxGQ~S?s6n`
zf~f3rQaqbw@2*=Q*DKw;)ITAe1y1S@)|GTAht3sr;M%6ISrrT6TqN?ZWbs~(&!IuR
z+x=fowyrqDDqXt7atl5}Z$cHEP=j?V;U$1p77X_l6=-G}Aq>Y7lI%qn&Ci1El4pQ+
z>qucT1*`Y?P}3yW_rio8Qjqv!>u4)zqm@-DKE$No1BV
zub!&fj_@y~Ab`c?QBhqNiW-bW?MN;<{t;ybq{RYn~$$jwXIZx
zJ_XbP_x0+a?vxRsubt6{?z7q`EcWLlaI^Nax;`6!>PaKB03EOJmketdAz_1N2Eb$G
zuofY0y~rB4^gLT2-NAa5U-cwiKm0g9qQ2O$mD3uD!;@ZR&z;pF%hoIemWI)0$FQMl
z9Iv+D`Y|29?0??dZ<{pKYN_Gqs2!7*Wnp>WskPQCc~O+YidCH+<@(6KH1Yaz6O)V@
zYrLObI_mEMvZJP^xz>VWf=WoMRl#|?^(ZhKq0*;`G89X7Gj#se9Zf7%F@195+JWT$
zc9=u<6sf#sPLb9#0BNh5xIa=YAFQjgSy