From 642842c29ee718f4926ead2619bd58ec2286c3aa Mon Sep 17 00:00:00 2001 From: shaw Date: Thu, 18 Dec 2025 13:50:39 +0800 Subject: [PATCH] First commit --- .dockerignore | 74 + .github/workflows/release.yml | 178 + .gitignore | 93 + .goreleaser.yaml | 85 + Dockerfile | 96 + README.md | 318 + README_CN.md | 318 + backend/Dockerfile | 24 + backend/cmd/server/VERSION | 1 + backend/cmd/server/main.go | 470 + backend/config.yaml | 38 + backend/go.mod | 74 + backend/go.sum | 187 + backend/internal/config/config.go | 205 + .../internal/handler/admin/account_handler.go | 537 + .../handler/admin/dashboard_handler.go | 274 + .../internal/handler/admin/group_handler.go | 233 + .../internal/handler/admin/proxy_handler.go | 300 + .../internal/handler/admin/redeem_handler.go | 219 + .../internal/handler/admin/setting_handler.go | 258 + .../handler/admin/subscription_handler.go | 266 + .../internal/handler/admin/system_handler.go | 82 + .../internal/handler/admin/usage_handler.go | 262 + .../internal/handler/admin/user_handler.go | 221 + backend/internal/handler/api_key_handler.go | 235 + backend/internal/handler/auth_handler.go | 158 + backend/internal/handler/gateway_handler.go | 445 + backend/internal/handler/handler.go | 70 + backend/internal/handler/redeem_handler.go | 92 + backend/internal/handler/setting_handler.go | 35 + .../internal/handler/subscription_handler.go | 203 + backend/internal/handler/usage_handler.go | 396 + backend/internal/handler/user_handler.go | 85 + backend/internal/middleware/admin_only.go | 28 + backend/internal/middleware/api_key_auth.go | 161 + backend/internal/middleware/cors.go | 24 + backend/internal/middleware/jwt_auth.go | 76 + backend/internal/middleware/logger.go | 52 + backend/internal/middleware/middleware.go | 35 + backend/internal/model/account.go | 265 + backend/internal/model/account_group.go | 20 + backend/internal/model/api_key.go | 32 + backend/internal/model/group.go | 73 + backend/internal/model/model.go | 64 + backend/internal/model/proxy.go | 45 + backend/internal/model/redeem_code.go | 47 + backend/internal/model/setting.go | 95 + backend/internal/model/usage_log.go | 67 + backend/internal/model/user.go | 74 + backend/internal/model/user_subscription.go | 157 + backend/internal/pkg/oauth/oauth.go | 223 + backend/internal/pkg/response/response.go | 157 + backend/internal/pkg/timezone/timezone.go | 124 + .../internal/pkg/timezone/timezone_test.go | 127 + backend/internal/repository/account_repo.go | 268 + backend/internal/repository/api_key_repo.go | 149 + backend/internal/repository/group_repo.go | 137 + backend/internal/repository/proxy_repo.go | 161 + .../internal/repository/redeem_code_repo.go | 133 + backend/internal/repository/repository.go | 74 + backend/internal/repository/setting_repo.go | 108 + backend/internal/repository/usage_log_repo.go | 1006 + backend/internal/repository/user_repo.go | 130 + .../repository/user_subscription_repo.go | 322 + backend/internal/service/account_service.go | 284 + .../internal/service/account_test_service.go | 314 + .../internal/service/account_usage_service.go | 345 + backend/internal/service/admin_service.go | 989 + backend/internal/service/api_key_service.go | 464 + backend/internal/service/auth_service.go | 376 + .../internal/service/billing_cache_service.go | 422 + backend/internal/service/billing_service.go | 279 + .../internal/service/concurrency_service.go | 251 + .../internal/service/email_queue_service.go | 109 + backend/internal/service/email_service.go | 372 + backend/internal/service/gateway_service.go | 1022 + backend/internal/service/group_service.go | 194 + backend/internal/service/identity_service.go | 282 + backend/internal/service/oauth_service.go | 471 + backend/internal/service/pricing_service.go | 572 + backend/internal/service/proxy_service.go | 192 + backend/internal/service/ratelimit_service.go | 170 + backend/internal/service/redeem_service.go | 392 + backend/internal/service/service.go | 139 + backend/internal/service/setting_service.go | 264 + .../internal/service/subscription_service.go | 575 + backend/internal/service/turnstile_service.go | 111 + backend/internal/service/update_service.go | 621 + backend/internal/service/usage_service.go | 283 + backend/internal/service/user_service.go | 177 + backend/internal/setup/cli.go | 294 + backend/internal/setup/handler.go | 344 + backend/internal/setup/setup.go | 564 + backend/internal/web/embed.go | 79 + backend/migrations/001_init.sql | 183 + .../migrations/002_account_type_migration.sql | 33 + backend/migrations/003_subscription.sql | 65 + backend/resources/model-pricing/README.md | 37 + .../model_prices_and_context_window.json | 31356 ++++++++++++++++ deploy/.env.example | 55 + deploy/README.md | 258 + deploy/config.example.yaml | 89 + deploy/docker-compose.yml | 160 + deploy/install.sh | 745 + deploy/sub2api-sudoers | 13 + deploy/sub2api.service | 33 + frontend/index.html | 13 + frontend/package-lock.json | 2932 ++ frontend/package.json | 32 + frontend/postcss.config.js | 6 + frontend/public/logo.png | Bin 0 -> 149928 bytes frontend/src/App.vue | 60 + frontend/src/api/admin/accounts.ts | 270 + frontend/src/api/admin/dashboard.ts | 173 + frontend/src/api/admin/groups.ts | 170 + frontend/src/api/admin/index.ts | 35 + frontend/src/api/admin/proxies.ts | 211 + frontend/src/api/admin/redeem.ts | 170 + frontend/src/api/admin/settings.ts | 109 + frontend/src/api/admin/subscriptions.ts | 157 + frontend/src/api/admin/system.ts | 48 + frontend/src/api/admin/usage.ts | 112 + frontend/src/api/admin/users.ts | 168 + frontend/src/api/auth.ts | 120 + frontend/src/api/client.ts | 89 + frontend/src/api/groups.ts | 25 + frontend/src/api/index.ts | 23 + frontend/src/api/keys.ts | 100 + frontend/src/api/redeem.ts | 65 + frontend/src/api/setup.ts | 87 + frontend/src/api/subscriptions.ts | 72 + frontend/src/api/usage.ts | 253 + frontend/src/api/user.ts | 41 + frontend/src/components/TurnstileWidget.vue | 176 + .../account/AccountStatusIndicator.vue | 120 + .../components/account/AccountTestModal.vue | 342 + .../account/AccountTodayStatsCell.vue | 82 + .../components/account/AccountUsageCell.vue | 113 + .../components/account/CreateAccountModal.vue | 929 + .../components/account/EditAccountModal.vue | 646 + .../account/OAuthAuthorizationFlow.vue | 364 + .../components/account/ReAuthAccountModal.vue | 240 + .../account/SetupTokenTimeWindow.vue | 200 + .../components/account/UsageProgressBar.vue | 129 + frontend/src/components/account/index.ts | 7 + .../src/components/common/ConfirmDialog.vue | 65 + frontend/src/components/common/DataTable.vue | 134 + .../src/components/common/DateRangePicker.vue | 415 + frontend/src/components/common/EmptyState.vue | 91 + frontend/src/components/common/GroupBadge.vue | 50 + .../src/components/common/GroupSelector.vue | 61 + .../src/components/common/LoadingSpinner.vue | 65 + .../src/components/common/LocaleSwitcher.vue | 100 + frontend/src/components/common/Modal.vue | 122 + frontend/src/components/common/Pagination.vue | 214 + .../src/components/common/ProxySelector.vue | 426 + frontend/src/components/common/README.md | 243 + frontend/src/components/common/Select.vue | 319 + frontend/src/components/common/StatCard.vue | 94 + .../common/SubscriptionProgressMini.vue | 267 + frontend/src/components/common/Toast.vue | 224 + frontend/src/components/common/Toggle.vue | 35 + .../src/components/common/VersionBadge.vue | 250 + frontend/src/components/common/index.ts | 13 + frontend/src/components/keys/UseKeyModal.vue | 200 + frontend/src/components/layout/AppHeader.vue | 259 + frontend/src/components/layout/AppLayout.vue | 36 + frontend/src/components/layout/AppSidebar.vue | 331 + frontend/src/components/layout/AuthLayout.vue | 77 + frontend/src/components/layout/EXAMPLES.md | 424 + frontend/src/components/layout/INTEGRATION.md | 480 + frontend/src/components/layout/README.md | 212 + frontend/src/components/layout/index.ts | 9 + frontend/src/composables/useAccountOAuth.ts | 176 + frontend/src/composables/useClipboard.ts | 40 + frontend/src/i18n/index.ts | 50 + frontend/src/i18n/locales/en.ts | 1054 + frontend/src/i18n/locales/zh.ts | 1233 + frontend/src/main.ts | 12 + frontend/src/router/README.md | 273 + frontend/src/router/index.ts | 345 + frontend/src/router/meta.d.ts | 46 + frontend/src/stores/README.md | 194 + frontend/src/stores/app.ts | 221 + frontend/src/stores/auth.ts | 219 + frontend/src/stores/index.ts | 11 + frontend/src/style.css | 521 + frontend/src/types/index.ts | 630 + frontend/src/utils/format.ts | 113 + frontend/src/views/HomeView.vue | 452 + frontend/src/views/NotFoundView.vue | 75 + frontend/src/views/admin/AccountsView.vue | 523 + frontend/src/views/admin/DashboardView.vue | 619 + frontend/src/views/admin/GroupsView.vue | 695 + frontend/src/views/admin/ProxiesView.vue | 827 + frontend/src/views/admin/RedeemView.vue | 645 + frontend/src/views/admin/SettingsView.vue | 559 + .../src/views/admin/SubscriptionsView.vue | 548 + frontend/src/views/admin/UsageView.vue | 593 + frontend/src/views/admin/UsersView.vue | 1002 + frontend/src/views/auth/EmailVerifyView.vue | 423 + frontend/src/views/auth/LoginView.vue | 328 + frontend/src/views/auth/README.md | 338 + frontend/src/views/auth/RegisterView.vue | 372 + frontend/src/views/auth/USAGE_EXAMPLES.md | 609 + frontend/src/views/auth/VISUAL_GUIDE.md | 591 + frontend/src/views/auth/index.ts | 7 + frontend/src/views/setup/SetupWizardView.vue | 400 + frontend/src/views/user/DashboardView.vue | 738 + frontend/src/views/user/KeysView.vue | 733 + frontend/src/views/user/ProfileView.vue | 253 + frontend/src/views/user/RedeemView.vue | 453 + frontend/src/views/user/SubscriptionsView.vue | 260 + frontend/src/views/user/UsageView.vue | 470 + frontend/tailwind.config.js | 138 + frontend/tsconfig.json | 24 + frontend/tsconfig.node.json | 10 + frontend/vite.config.ts | 30 + 218 files changed, 86902 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 .goreleaser.yaml create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 README_CN.md create mode 100644 backend/Dockerfile create mode 100644 backend/cmd/server/VERSION create mode 100644 backend/cmd/server/main.go create mode 100644 backend/config.yaml create mode 100644 backend/go.mod create mode 100644 backend/go.sum create mode 100644 backend/internal/config/config.go create mode 100644 backend/internal/handler/admin/account_handler.go create mode 100644 backend/internal/handler/admin/dashboard_handler.go create mode 100644 backend/internal/handler/admin/group_handler.go create mode 100644 backend/internal/handler/admin/proxy_handler.go create mode 100644 backend/internal/handler/admin/redeem_handler.go create mode 100644 backend/internal/handler/admin/setting_handler.go create mode 100644 backend/internal/handler/admin/subscription_handler.go create mode 100644 backend/internal/handler/admin/system_handler.go create mode 100644 backend/internal/handler/admin/usage_handler.go create mode 100644 backend/internal/handler/admin/user_handler.go create mode 100644 backend/internal/handler/api_key_handler.go create mode 100644 backend/internal/handler/auth_handler.go create mode 100644 backend/internal/handler/gateway_handler.go create mode 100644 backend/internal/handler/handler.go create mode 100644 backend/internal/handler/redeem_handler.go create mode 100644 backend/internal/handler/setting_handler.go create mode 100644 backend/internal/handler/subscription_handler.go create mode 100644 backend/internal/handler/usage_handler.go create mode 100644 backend/internal/handler/user_handler.go create mode 100644 backend/internal/middleware/admin_only.go create mode 100644 backend/internal/middleware/api_key_auth.go create mode 100644 backend/internal/middleware/cors.go create mode 100644 backend/internal/middleware/jwt_auth.go create mode 100644 backend/internal/middleware/logger.go create mode 100644 backend/internal/middleware/middleware.go create mode 100644 backend/internal/model/account.go create mode 100644 backend/internal/model/account_group.go create mode 100644 backend/internal/model/api_key.go create mode 100644 backend/internal/model/group.go create mode 100644 backend/internal/model/model.go create mode 100644 backend/internal/model/proxy.go create mode 100644 backend/internal/model/redeem_code.go create mode 100644 backend/internal/model/setting.go create mode 100644 backend/internal/model/usage_log.go create mode 100644 backend/internal/model/user.go create mode 100644 backend/internal/model/user_subscription.go create mode 100644 backend/internal/pkg/oauth/oauth.go create mode 100644 backend/internal/pkg/response/response.go create mode 100644 backend/internal/pkg/timezone/timezone.go create mode 100644 backend/internal/pkg/timezone/timezone_test.go create mode 100644 backend/internal/repository/account_repo.go create mode 100644 backend/internal/repository/api_key_repo.go create mode 100644 backend/internal/repository/group_repo.go create mode 100644 backend/internal/repository/proxy_repo.go create mode 100644 backend/internal/repository/redeem_code_repo.go create mode 100644 backend/internal/repository/repository.go create mode 100644 backend/internal/repository/setting_repo.go create mode 100644 backend/internal/repository/usage_log_repo.go create mode 100644 backend/internal/repository/user_repo.go create mode 100644 backend/internal/repository/user_subscription_repo.go create mode 100644 backend/internal/service/account_service.go create mode 100644 backend/internal/service/account_test_service.go create mode 100644 backend/internal/service/account_usage_service.go create mode 100644 backend/internal/service/admin_service.go create mode 100644 backend/internal/service/api_key_service.go create mode 100644 backend/internal/service/auth_service.go create mode 100644 backend/internal/service/billing_cache_service.go create mode 100644 backend/internal/service/billing_service.go create mode 100644 backend/internal/service/concurrency_service.go create mode 100644 backend/internal/service/email_queue_service.go create mode 100644 backend/internal/service/email_service.go create mode 100644 backend/internal/service/gateway_service.go create mode 100644 backend/internal/service/group_service.go create mode 100644 backend/internal/service/identity_service.go create mode 100644 backend/internal/service/oauth_service.go create mode 100644 backend/internal/service/pricing_service.go create mode 100644 backend/internal/service/proxy_service.go create mode 100644 backend/internal/service/ratelimit_service.go create mode 100644 backend/internal/service/redeem_service.go create mode 100644 backend/internal/service/service.go create mode 100644 backend/internal/service/setting_service.go create mode 100644 backend/internal/service/subscription_service.go create mode 100644 backend/internal/service/turnstile_service.go create mode 100644 backend/internal/service/update_service.go create mode 100644 backend/internal/service/usage_service.go create mode 100644 backend/internal/service/user_service.go create mode 100644 backend/internal/setup/cli.go create mode 100644 backend/internal/setup/handler.go create mode 100644 backend/internal/setup/setup.go create mode 100644 backend/internal/web/embed.go create mode 100644 backend/migrations/001_init.sql create mode 100644 backend/migrations/002_account_type_migration.sql create mode 100644 backend/migrations/003_subscription.sql create mode 100644 backend/resources/model-pricing/README.md create mode 100644 backend/resources/model-pricing/model_prices_and_context_window.json create mode 100644 deploy/.env.example create mode 100644 deploy/README.md create mode 100644 deploy/config.example.yaml create mode 100644 deploy/docker-compose.yml create mode 100644 deploy/install.sh create mode 100644 deploy/sub2api-sudoers create mode 100644 deploy/sub2api.service create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/public/logo.png create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/api/admin/accounts.ts create mode 100644 frontend/src/api/admin/dashboard.ts create mode 100644 frontend/src/api/admin/groups.ts create mode 100644 frontend/src/api/admin/index.ts create mode 100644 frontend/src/api/admin/proxies.ts create mode 100644 frontend/src/api/admin/redeem.ts create mode 100644 frontend/src/api/admin/settings.ts create mode 100644 frontend/src/api/admin/subscriptions.ts create mode 100644 frontend/src/api/admin/system.ts create mode 100644 frontend/src/api/admin/usage.ts create mode 100644 frontend/src/api/admin/users.ts create mode 100644 frontend/src/api/auth.ts create mode 100644 frontend/src/api/client.ts create mode 100644 frontend/src/api/groups.ts create mode 100644 frontend/src/api/index.ts create mode 100644 frontend/src/api/keys.ts create mode 100644 frontend/src/api/redeem.ts create mode 100644 frontend/src/api/setup.ts create mode 100644 frontend/src/api/subscriptions.ts create mode 100644 frontend/src/api/usage.ts create mode 100644 frontend/src/api/user.ts create mode 100644 frontend/src/components/TurnstileWidget.vue create mode 100644 frontend/src/components/account/AccountStatusIndicator.vue create mode 100644 frontend/src/components/account/AccountTestModal.vue create mode 100644 frontend/src/components/account/AccountTodayStatsCell.vue create mode 100644 frontend/src/components/account/AccountUsageCell.vue create mode 100644 frontend/src/components/account/CreateAccountModal.vue create mode 100644 frontend/src/components/account/EditAccountModal.vue create mode 100644 frontend/src/components/account/OAuthAuthorizationFlow.vue create mode 100644 frontend/src/components/account/ReAuthAccountModal.vue create mode 100644 frontend/src/components/account/SetupTokenTimeWindow.vue create mode 100644 frontend/src/components/account/UsageProgressBar.vue create mode 100644 frontend/src/components/account/index.ts create mode 100644 frontend/src/components/common/ConfirmDialog.vue create mode 100644 frontend/src/components/common/DataTable.vue create mode 100644 frontend/src/components/common/DateRangePicker.vue create mode 100644 frontend/src/components/common/EmptyState.vue create mode 100644 frontend/src/components/common/GroupBadge.vue create mode 100644 frontend/src/components/common/GroupSelector.vue create mode 100644 frontend/src/components/common/LoadingSpinner.vue create mode 100644 frontend/src/components/common/LocaleSwitcher.vue create mode 100644 frontend/src/components/common/Modal.vue create mode 100644 frontend/src/components/common/Pagination.vue create mode 100644 frontend/src/components/common/ProxySelector.vue create mode 100644 frontend/src/components/common/README.md create mode 100644 frontend/src/components/common/Select.vue create mode 100644 frontend/src/components/common/StatCard.vue create mode 100644 frontend/src/components/common/SubscriptionProgressMini.vue create mode 100644 frontend/src/components/common/Toast.vue create mode 100644 frontend/src/components/common/Toggle.vue create mode 100644 frontend/src/components/common/VersionBadge.vue create mode 100644 frontend/src/components/common/index.ts create mode 100644 frontend/src/components/keys/UseKeyModal.vue create mode 100644 frontend/src/components/layout/AppHeader.vue create mode 100644 frontend/src/components/layout/AppLayout.vue create mode 100644 frontend/src/components/layout/AppSidebar.vue create mode 100644 frontend/src/components/layout/AuthLayout.vue create mode 100644 frontend/src/components/layout/EXAMPLES.md create mode 100644 frontend/src/components/layout/INTEGRATION.md create mode 100644 frontend/src/components/layout/README.md create mode 100644 frontend/src/components/layout/index.ts create mode 100644 frontend/src/composables/useAccountOAuth.ts create mode 100644 frontend/src/composables/useClipboard.ts create mode 100644 frontend/src/i18n/index.ts create mode 100644 frontend/src/i18n/locales/en.ts create mode 100644 frontend/src/i18n/locales/zh.ts create mode 100644 frontend/src/main.ts create mode 100644 frontend/src/router/README.md create mode 100644 frontend/src/router/index.ts create mode 100644 frontend/src/router/meta.d.ts create mode 100644 frontend/src/stores/README.md create mode 100644 frontend/src/stores/app.ts create mode 100644 frontend/src/stores/auth.ts create mode 100644 frontend/src/stores/index.ts create mode 100644 frontend/src/style.css create mode 100644 frontend/src/types/index.ts create mode 100644 frontend/src/utils/format.ts create mode 100644 frontend/src/views/HomeView.vue create mode 100644 frontend/src/views/NotFoundView.vue create mode 100644 frontend/src/views/admin/AccountsView.vue create mode 100644 frontend/src/views/admin/DashboardView.vue create mode 100644 frontend/src/views/admin/GroupsView.vue create mode 100644 frontend/src/views/admin/ProxiesView.vue create mode 100644 frontend/src/views/admin/RedeemView.vue create mode 100644 frontend/src/views/admin/SettingsView.vue create mode 100644 frontend/src/views/admin/SubscriptionsView.vue create mode 100644 frontend/src/views/admin/UsageView.vue create mode 100644 frontend/src/views/admin/UsersView.vue create mode 100644 frontend/src/views/auth/EmailVerifyView.vue create mode 100644 frontend/src/views/auth/LoginView.vue create mode 100644 frontend/src/views/auth/README.md create mode 100644 frontend/src/views/auth/RegisterView.vue create mode 100644 frontend/src/views/auth/USAGE_EXAMPLES.md create mode 100644 frontend/src/views/auth/VISUAL_GUIDE.md create mode 100644 frontend/src/views/auth/index.ts create mode 100644 frontend/src/views/setup/SetupWizardView.vue create mode 100644 frontend/src/views/user/DashboardView.vue create mode 100644 frontend/src/views/user/KeysView.vue create mode 100644 frontend/src/views/user/ProfileView.vue create mode 100644 frontend/src/views/user/RedeemView.vue create mode 100644 frontend/src/views/user/SubscriptionsView.vue create mode 100644 frontend/src/views/user/UsageView.vue create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..ab803d44 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,74 @@ +# ============================================================================= +# Docker Ignore File for Sub2API +# ============================================================================= + +# Git +.git +.gitignore +.gitattributes + +# Documentation +*.md +!deploy/DOCKER.md +docs/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db + +# Build artifacts +dist/ +build/ + +# Node modules (will be installed in container) +frontend/node_modules/ +node_modules/ + +# Go build cache (will be built in container) +backend/vendor/ + +# Test files +*_test.go +**/*.test.js +coverage/ +.nyc_output/ + +# Environment files +.env +.env.* +!.env.example + +# Local config +config.yaml +config.local.yaml + +# Logs +*.log +logs/ + +# Temporary files +tmp/ +temp/ +*.tmp + +# Deploy files (not needed in image) +deploy/install.sh +deploy/sub2api.service +deploy/sub2api-sudoers + +# GoReleaser +.goreleaser.yaml + +# GitHub +.github/ + +# Claude files +.claude/ +issues/ +CLAUDE.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..9127215f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,178 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + packages: write + +jobs: + # Update VERSION file with tag version + update-version: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Update VERSION file + run: | + VERSION=${GITHUB_REF#refs/tags/v} + echo "$VERSION" > backend/cmd/server/VERSION + echo "Updated VERSION file to: $VERSION" + + - name: Upload VERSION artifact + uses: actions/upload-artifact@v4 + with: + name: version-file + path: backend/cmd/server/VERSION + retention-days: 1 + + build-frontend: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci + working-directory: frontend + + - name: Build frontend + run: npm run build + working-directory: frontend + + - name: Upload frontend artifact + uses: actions/upload-artifact@v4 + with: + name: frontend-dist + path: backend/internal/web/dist/ + retention-days: 1 + + release: + needs: [update-version, build-frontend] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download VERSION artifact + uses: actions/download-artifact@v4 + with: + name: version-file + path: backend/cmd/server/ + + - name: Download frontend artifact + uses: actions/download-artifact@v4 + with: + name: frontend-dist + path: backend/internal/web/dist/ + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + cache-dependency-path: backend/go.sum + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + version: '~> v2' + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # =========================================================================== + # Docker Build and Push + # =========================================================================== + docker: + needs: [update-version, build-frontend] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download VERSION artifact + uses: actions/download-artifact@v4 + with: + name: version-file + path: backend/cmd/server/ + + - name: Download frontend artifact + uses: actions/download-artifact@v4 + with: + name: frontend-dist + path: backend/internal/web/dist/ + + # Extract version from tag + - name: Extract version + id: version + run: | + VERSION=${GITHUB_REF#refs/tags/v} + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Version: $VERSION" + + # Set up Docker Buildx for multi-platform builds + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # Login to DockerHub + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + # Extract metadata for Docker + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + weishaw/sub2api + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable={{is_default_branch}} + + # Build and push Docker image + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + VERSION=${{ steps.version.outputs.version }} + COMMIT=${{ github.sha }} + DATE=${{ github.event.head_commit.timestamp }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # Update DockerHub description (optional) + - name: Update DockerHub description + uses: peter-evans/dockerhub-description@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + repository: weishaw/sub2api + short-description: "Sub2API - AI API Gateway Platform" + readme-filepath: ./deploy/DOCKER.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..60959006 --- /dev/null +++ b/.gitignore @@ -0,0 +1,93 @@ +docs/claude-relay-service/ + +# =================== +# Go 后端 +# =================== +# 二进制文件 +*.exe +*.exe~ +*.dll +*.so +*.dylib +backend/bin/ +backend/server +backend/sub2api + +# 测试覆盖率 +*.out +coverage.html + +# 依赖(使用 go mod) +vendor/ + +# =================== +# Node.js / Vue 前端 +# =================== +node_modules/ +frontend/node_modules/ +frontend/dist/ +*.local + +# 日志 +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# =================== +# 环境配置 +# =================== +.env +.env.local +.env.*.local +*.env +!.env.example + +# =================== +# IDE / 编辑器 +# =================== +.idea/ +.vscode/ +*.swp +*.swo +*~ +.project +.settings/ +.classpath + +# =================== +# 操作系统 +# =================== +.DS_Store +Thumbs.db +Desktop.ini + +# =================== +# 临时文件 +# =================== +tmp/ +temp/ +*.tmp +*.temp +*.log +*.bak + +# =================== +# 构建产物 +# =================== +dist/ +build/ +release/ + +# 后端嵌入的前端构建产物 +backend/internal/web/dist/ + +# 后端运行时缓存数据 +backend/data/ + +# =================== +# 其他 +# =================== +tests +CLAUDE.md +.claude \ No newline at end of file diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 00000000..c90bf97e --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,85 @@ +version: 2 + +project_name: sub2api + +before: + hooks: + - go mod tidy -C backend + +builds: + - id: sub2api + dir: backend + main: ./cmd/server + binary: sub2api + env: + - CGO_ENABLED=0 + goos: + - linux + - windows + - darwin + goarch: + - amd64 + - arm64 + ignore: + - goos: windows + goarch: arm64 + ldflags: + - -s -w + - -X main.Commit={{.Commit}} + - -X main.Date={{.Date}} + - -X main.BuildType=release + +archives: + - id: default + format: tar.gz + name_template: >- + {{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }} + format_overrides: + - goos: windows + format: zip + files: + - LICENSE* + - README* + - deploy/* + +checksum: + name_template: 'checksums.txt' + algorithm: sha256 + +changelog: + # 禁用自动 changelog,完全使用 tag 消息 + disable: true + +release: + github: + owner: Wei-Shaw + name: sub2api + draft: false + prerelease: auto + name_template: "v{{.Version}}" + # 完全使用 tag 消息作为 release 内容 + header: | + ## Sub2API {{.Version}} + + > AI API Gateway Platform - 将 AI 订阅配额分发和管理 + + {{ .TagBody }} + + footer: | + + --- + + ## 📥 Installation + + **One-line install (Linux):** + ```bash + curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install.sh | sudo bash + ``` + + **Manual download:** + Download the appropriate archive for your platform from the assets below. + + ## 📚 Documentation + + - [GitHub Repository](https://github.com/Wei-Shaw/sub2api) + - [Installation Guide](https://github.com/Wei-Shaw/sub2api/blob/main/deploy/README.md) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..d50e2a0a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,96 @@ +# ============================================================================= +# Sub2API Multi-Stage Dockerfile +# ============================================================================= +# Stage 1: Build frontend +# Stage 2: Build Go backend with embedded frontend +# Stage 3: Final minimal image +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Stage 1: Frontend Builder +# ----------------------------------------------------------------------------- +FROM node:20-alpine AS frontend-builder + +WORKDIR /app/frontend + +# Install dependencies first (better caching) +COPY frontend/package*.json ./ +RUN npm ci + +# Copy frontend source and build +COPY frontend/ ./ +RUN npm run build + +# ----------------------------------------------------------------------------- +# Stage 2: Backend Builder +# ----------------------------------------------------------------------------- +FROM golang:1.24-alpine AS backend-builder + +# Build arguments for version info (set by CI) +ARG VERSION=docker +ARG COMMIT=docker +ARG DATE + +# Install build dependencies +RUN apk add --no-cache git ca-certificates tzdata + +WORKDIR /app/backend + +# Copy go mod files first (better caching) +COPY backend/go.mod backend/go.sum ./ +RUN go mod download + +# Copy frontend dist from previous stage +COPY --from=frontend-builder /app/frontend/../backend/internal/web/dist ./internal/web/dist + +# Copy backend source +COPY backend/ ./ + +# Build the binary (BuildType=release for CI builds) +RUN CGO_ENABLED=0 GOOS=linux go build \ + -ldflags="-s -w -X main.Commit=${COMMIT} -X main.Date=${DATE:-$(date -u +%Y-%m-%dT%H:%M:%SZ)} -X main.BuildType=release" \ + -o /app/sub2api \ + ./cmd/server + +# ----------------------------------------------------------------------------- +# Stage 3: Final Runtime Image +# ----------------------------------------------------------------------------- +FROM alpine:3.19 + +# Labels +LABEL maintainer="Wei-Shaw " +LABEL description="Sub2API - AI API Gateway Platform" +LABEL org.opencontainers.image.source="https://github.com/Wei-Shaw/sub2api" + +# Install runtime dependencies +RUN apk add --no-cache \ + ca-certificates \ + tzdata \ + curl \ + && rm -rf /var/cache/apk/* + +# Create non-root user +RUN addgroup -g 1000 sub2api && \ + adduser -u 1000 -G sub2api -s /bin/sh -D sub2api + +# Set working directory +WORKDIR /app + +# Copy binary from builder +COPY --from=backend-builder /app/sub2api /app/sub2api + +# Create data directory +RUN mkdir -p /app/data && chown -R sub2api:sub2api /app + +# Switch to non-root user +USER sub2api + +# Expose port (can be overridden by SERVER_PORT env var) +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ + CMD curl -f http://localhost:${SERVER_PORT:-8080}/health || exit 1 + +# Run the application +ENTRYPOINT ["/app/sub2api"] diff --git a/README.md b/README.md new file mode 100644 index 00000000..a0988228 --- /dev/null +++ b/README.md @@ -0,0 +1,318 @@ +# Sub2API + +
+ +[![Go](https://img.shields.io/badge/Go-1.21+-00ADD8.svg)](https://golang.org/) +[![Vue](https://img.shields.io/badge/Vue-3.4+-4FC08D.svg)](https://vuejs.org/) +[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-336791.svg)](https://www.postgresql.org/) +[![Redis](https://img.shields.io/badge/Redis-7+-DC382D.svg)](https://redis.io/) +[![Docker](https://img.shields.io/badge/Docker-Ready-2496ED.svg)](https://www.docker.com/) + +**AI API Gateway Platform for Subscription Quota Distribution** + +English | [中文](README_CN.md) + +
+ +--- + +## Overview + +Sub2API is an AI API gateway platform designed to distribute and manage API quotas from AI product subscriptions (like Claude Code $200/month). Users can access upstream AI services through platform-generated API Keys, while the platform handles authentication, billing, load balancing, and request forwarding. + +## Features + +- **Multi-Account Management** - Support multiple upstream account types (OAuth, API Key) +- **API Key Distribution** - Generate and manage API Keys for users +- **Precise Billing** - Token-level usage tracking and cost calculation +- **Smart Scheduling** - Intelligent account selection with sticky sessions +- **Concurrency Control** - Per-user and per-account concurrency limits +- **Rate Limiting** - Configurable request and token rate limits +- **Admin Dashboard** - Web interface for monitoring and management + +## Tech Stack + +| Component | Technology | +|-----------|------------| +| Backend | Go 1.21+, Gin, GORM | +| Frontend | Vue 3.4+, Vite 5+, TailwindCSS | +| Database | PostgreSQL 15+ | +| Cache/Queue | Redis 7+ | + +--- + +## Deployment + +### Method 1: Script Installation (Recommended) + +One-click installation script that downloads pre-built binaries from GitHub Releases. + +#### Prerequisites + +- Linux server (amd64 or arm64) +- PostgreSQL 15+ (installed and running) +- Redis 7+ (installed and running) +- Root privileges + +#### Installation Steps + +```bash +# Download and run the installation script +curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install.sh | sudo bash +``` + +The script will: +1. Detect your system architecture +2. Download the latest release +3. Install binary to `/opt/sub2api` +4. Create systemd service +5. Configure system user and permissions + +#### Post-Installation + +```bash +# 1. Start the service +sudo systemctl start sub2api + +# 2. Enable auto-start on boot +sudo systemctl enable sub2api + +# 3. Open Setup Wizard in browser +# http://YOUR_SERVER_IP:8080 +``` + +The Setup Wizard will guide you through: +- Database configuration +- Redis configuration +- Admin account creation + +#### Upgrade + +You can upgrade directly from the **Admin Dashboard** by clicking the **Check for Updates** button in the top-left corner. + +The web interface will: +- Check for new versions automatically +- Download and apply updates with one click +- Support rollback if needed + +#### Useful Commands + +```bash +# Check status +sudo systemctl status sub2api + +# View logs +sudo journalctl -u sub2api -f + +# Restart service +sudo systemctl restart sub2api + +# Uninstall +curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install.sh | sudo bash -s uninstall +``` + +--- + +### Method 2: Docker Compose + +Deploy with Docker Compose, including PostgreSQL and Redis containers. + +#### Prerequisites + +- Docker 20.10+ +- Docker Compose v2+ + +#### Installation Steps + +```bash +# 1. Clone the repository +git clone https://github.com/Wei-Shaw/sub2api.git +cd sub2api + +# 2. Enter the deploy directory +cd deploy + +# 3. Copy environment configuration +cp .env.example .env + +# 4. Edit configuration (set your passwords) +nano .env +``` + +**Required configuration in `.env`:** + +```bash +# PostgreSQL password (REQUIRED - change this!) +POSTGRES_PASSWORD=your_secure_password_here + +# Optional: Admin account +ADMIN_EMAIL=admin@example.com +ADMIN_PASSWORD=your_admin_password + +# Optional: Custom port +SERVER_PORT=8080 +``` + +```bash +# 5. Start all services +docker-compose up -d + +# 6. Check status +docker-compose ps + +# 7. View logs +docker-compose logs -f sub2api +``` + +#### Access + +Open `http://YOUR_SERVER_IP:8080` in your browser. + +#### Upgrade + +```bash +# Pull latest image and recreate container +docker-compose pull +docker-compose up -d +``` + +#### Useful Commands + +```bash +# Stop all services +docker-compose down + +# Restart +docker-compose restart + +# View all logs +docker-compose logs -f +``` + +--- + +### Method 3: Build from Source + +Build and run from source code for development or customization. + +#### Prerequisites + +- Go 1.21+ +- Node.js 18+ +- PostgreSQL 15+ +- Redis 7+ + +#### Build Steps + +```bash +# 1. Clone the repository +git clone https://github.com/Wei-Shaw/sub2api.git +cd sub2api + +# 2. Build backend +cd backend +go build -o sub2api ./cmd/server + +# 3. Build frontend +cd ../frontend +npm install +npm run build + +# 4. Copy frontend build to backend (for embedding) +cp -r dist ../backend/internal/web/ + +# 5. Create configuration file +cd ../backend +cp ../deploy/config.example.yaml ./config.yaml + +# 6. Edit configuration +nano config.yaml +``` + +**Key configuration in `config.yaml`:** + +```yaml +server: + host: "0.0.0.0" + port: 8080 + mode: "release" + +database: + host: "localhost" + port: 5432 + user: "postgres" + password: "your_password" + dbname: "sub2api" + +redis: + host: "localhost" + port: 6379 + password: "" + +jwt: + secret: "change-this-to-a-secure-random-string" + expire_hour: 24 + +default: + admin_email: "admin@example.com" + admin_password: "admin123" +``` + +```bash +# 7. Run the application +./sub2api +``` + +#### Development Mode + +```bash +# Backend (with hot reload) +cd backend +go run ./cmd/server + +# Frontend (with hot reload) +cd frontend +npm run dev +``` + +--- + +## Project Structure + +``` +sub2api/ +├── backend/ # Go backend service +│ ├── cmd/server/ # Application entry +│ ├── internal/ # Internal modules +│ │ ├── config/ # Configuration +│ │ ├── model/ # Data models +│ │ ├── service/ # Business logic +│ │ ├── handler/ # HTTP handlers +│ │ └── gateway/ # API gateway core +│ └── resources/ # Static resources +│ +├── frontend/ # Vue 3 frontend +│ └── src/ +│ ├── api/ # API calls +│ ├── stores/ # State management +│ ├── views/ # Page components +│ └── components/ # Reusable components +│ +└── deploy/ # Deployment files + ├── docker-compose.yml # Docker Compose configuration + ├── .env.example # Environment variables for Docker Compose + ├── config.example.yaml # Full config file for binary deployment + └── install.sh # One-click installation script +``` + +## License + +MIT License + +--- + +
+ +**If you find this project useful, please give it a star!** + +
diff --git a/README_CN.md b/README_CN.md new file mode 100644 index 00000000..db25c4e4 --- /dev/null +++ b/README_CN.md @@ -0,0 +1,318 @@ +# Sub2API + +
+ +[![Go](https://img.shields.io/badge/Go-1.21+-00ADD8.svg)](https://golang.org/) +[![Vue](https://img.shields.io/badge/Vue-3.4+-4FC08D.svg)](https://vuejs.org/) +[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-336791.svg)](https://www.postgresql.org/) +[![Redis](https://img.shields.io/badge/Redis-7+-DC382D.svg)](https://redis.io/) +[![Docker](https://img.shields.io/badge/Docker-Ready-2496ED.svg)](https://www.docker.com/) + +**AI API 网关平台 - 订阅配额分发管理** + +[English](README.md) | 中文 + +
+ +--- + +## 项目概述 + +Sub2API 是一个 AI API 网关平台,用于分发和管理 AI 产品订阅(如 Claude Code $200/月)的 API 配额。用户通过平台生成的 API Key 调用上游 AI 服务,平台负责鉴权、计费、负载均衡和请求转发。 + +## 核心功能 + +- **多账号管理** - 支持多种上游账号类型(OAuth、API Key) +- **API Key 分发** - 为用户生成和管理 API Key +- **精确计费** - Token 级别的用量追踪和成本计算 +- **智能调度** - 智能账号选择,支持粘性会话 +- **并发控制** - 用户级和账号级并发限制 +- **速率限制** - 可配置的请求和 Token 速率限制 +- **管理后台** - Web 界面进行监控和管理 + +## 技术栈 + +| 组件 | 技术 | +|------|------| +| 后端 | Go 1.21+, Gin, GORM | +| 前端 | Vue 3.4+, Vite 5+, TailwindCSS | +| 数据库 | PostgreSQL 15+ | +| 缓存/队列 | Redis 7+ | + +--- + +## 部署方式 + +### 方式一:脚本安装(推荐) + +一键安装脚本,自动从 GitHub Releases 下载预编译的二进制文件。 + +#### 前置条件 + +- Linux 服务器(amd64 或 arm64) +- PostgreSQL 15+(已安装并运行) +- Redis 7+(已安装并运行) +- Root 权限 + +#### 安装步骤 + +```bash +# 下载并运行安装脚本 +curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install.sh | sudo bash +``` + +脚本会自动: +1. 检测系统架构 +2. 下载最新版本 +3. 安装二进制文件到 `/opt/sub2api` +4. 创建 systemd 服务 +5. 配置系统用户和权限 + +#### 安装后配置 + +```bash +# 1. 启动服务 +sudo systemctl start sub2api + +# 2. 设置开机自启 +sudo systemctl enable sub2api + +# 3. 在浏览器中打开设置向导 +# http://你的服务器IP:8080 +``` + +设置向导将引导你完成: +- 数据库配置 +- Redis 配置 +- 管理员账号创建 + +#### 升级 + +可以直接在 **管理后台** 左上角点击 **检测更新** 按钮进行在线升级。 + +网页升级功能支持: +- 自动检测新版本 +- 一键下载并应用更新 +- 支持回滚 + +#### 常用命令 + +```bash +# 查看状态 +sudo systemctl status sub2api + +# 查看日志 +sudo journalctl -u sub2api -f + +# 重启服务 +sudo systemctl restart sub2api + +# 卸载 +curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install.sh | sudo bash -s uninstall +``` + +--- + +### 方式二:Docker Compose + +使用 Docker Compose 部署,包含 PostgreSQL 和 Redis 容器。 + +#### 前置条件 + +- Docker 20.10+ +- Docker Compose v2+ + +#### 安装步骤 + +```bash +# 1. 克隆仓库 +git clone https://github.com/Wei-Shaw/sub2api.git +cd sub2api + +# 2. 进入 deploy 目录 +cd deploy + +# 3. 复制环境配置文件 +cp .env.example .env + +# 4. 编辑配置(设置密码等) +nano .env +``` + +**`.env` 必须配置项:** + +```bash +# PostgreSQL 密码(必须修改!) +POSTGRES_PASSWORD=your_secure_password_here + +# 可选:管理员账号 +ADMIN_EMAIL=admin@example.com +ADMIN_PASSWORD=your_admin_password + +# 可选:自定义端口 +SERVER_PORT=8080 +``` + +```bash +# 5. 启动所有服务 +docker-compose up -d + +# 6. 查看状态 +docker-compose ps + +# 7. 查看日志 +docker-compose logs -f sub2api +``` + +#### 访问 + +在浏览器中打开 `http://你的服务器IP:8080` + +#### 升级 + +```bash +# 拉取最新镜像并重建容器 +docker-compose pull +docker-compose up -d +``` + +#### 常用命令 + +```bash +# 停止所有服务 +docker-compose down + +# 重启 +docker-compose restart + +# 查看所有日志 +docker-compose logs -f +``` + +--- + +### 方式三:源码编译 + +从源码编译安装,适合开发或定制需求。 + +#### 前置条件 + +- Go 1.21+ +- Node.js 18+ +- PostgreSQL 15+ +- Redis 7+ + +#### 编译步骤 + +```bash +# 1. 克隆仓库 +git clone https://github.com/Wei-Shaw/sub2api.git +cd sub2api + +# 2. 编译后端 +cd backend +go build -o sub2api ./cmd/server + +# 3. 编译前端 +cd ../frontend +npm install +npm run build + +# 4. 复制前端构建产物到后端(用于嵌入) +cp -r dist ../backend/internal/web/ + +# 5. 创建配置文件 +cd ../backend +cp ../deploy/config.example.yaml ./config.yaml + +# 6. 编辑配置 +nano config.yaml +``` + +**`config.yaml` 关键配置:** + +```yaml +server: + host: "0.0.0.0" + port: 8080 + mode: "release" + +database: + host: "localhost" + port: 5432 + user: "postgres" + password: "your_password" + dbname: "sub2api" + +redis: + host: "localhost" + port: 6379 + password: "" + +jwt: + secret: "change-this-to-a-secure-random-string" + expire_hour: 24 + +default: + admin_email: "admin@example.com" + admin_password: "admin123" +``` + +```bash +# 7. 运行应用 +./sub2api +``` + +#### 开发模式 + +```bash +# 后端(支持热重载) +cd backend +go run ./cmd/server + +# 前端(支持热重载) +cd frontend +npm run dev +``` + +--- + +## 项目结构 + +``` +sub2api/ +├── backend/ # Go 后端服务 +│ ├── cmd/server/ # 应用入口 +│ ├── internal/ # 内部模块 +│ │ ├── config/ # 配置管理 +│ │ ├── model/ # 数据模型 +│ │ ├── service/ # 业务逻辑 +│ │ ├── handler/ # HTTP 处理器 +│ │ └── gateway/ # API 网关核心 +│ └── resources/ # 静态资源 +│ +├── frontend/ # Vue 3 前端 +│ └── src/ +│ ├── api/ # API 调用 +│ ├── stores/ # 状态管理 +│ ├── views/ # 页面组件 +│ └── components/ # 通用组件 +│ +└── deploy/ # 部署文件 + ├── docker-compose.yml # Docker Compose 配置 + ├── .env.example # Docker Compose 环境变量 + ├── config.example.yaml # 二进制部署完整配置文件 + └── install.sh # 一键安装脚本 +``` + +## 许可证 + +MIT License + +--- + +
+ +**如果觉得有用,请给个 Star 支持一下!** + +
diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 00000000..3bc4e50f --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,24 @@ +FROM golang:1.21-alpine + +WORKDIR /app + +# 安装必要的工具 +RUN apk add --no-cache git + +# 复制go.mod和go.sum +COPY go.mod go.sum ./ + +# 下载依赖 +RUN go mod download + +# 复制源代码 +COPY . . + +# 构建应用 +RUN go build -o main cmd/server/main.go + +# 暴露端口 +EXPOSE 8080 + +# 运行应用 +CMD ["./main"] diff --git a/backend/cmd/server/VERSION b/backend/cmd/server/VERSION new file mode 100644 index 00000000..17e51c38 --- /dev/null +++ b/backend/cmd/server/VERSION @@ -0,0 +1 @@ +0.1.1 diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go new file mode 100644 index 00000000..e020c218 --- /dev/null +++ b/backend/cmd/server/main.go @@ -0,0 +1,470 @@ +package main + +import ( + "context" + _ "embed" + "flag" + "log" + "net/http" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "sub2api/internal/config" + "sub2api/internal/handler" + "sub2api/internal/middleware" + "sub2api/internal/model" + "sub2api/internal/pkg/timezone" + "sub2api/internal/repository" + "sub2api/internal/service" + "sub2api/internal/setup" + "sub2api/internal/web" + + "github.com/gin-gonic/gin" + "github.com/redis/go-redis/v9" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +//go:embed VERSION +var embeddedVersion string + +// Build-time variables (can be set by ldflags) +var ( + Version = "" + Commit = "unknown" + Date = "unknown" + BuildType = "source" // "source" for manual builds, "release" for CI builds (set by ldflags) +) + +func init() { + // Read version from embedded VERSION file + Version = strings.TrimSpace(embeddedVersion) + if Version == "" { + Version = "0.0.0-dev" + } +} + +func main() { + // Parse command line flags + setupMode := flag.Bool("setup", false, "Run setup wizard in CLI mode") + showVersion := flag.Bool("version", false, "Show version information") + flag.Parse() + + if *showVersion { + log.Printf("Sub2API %s (commit: %s, built: %s)\n", Version, Commit, Date) + return + } + + // CLI setup mode + if *setupMode { + if err := setup.RunCLI(); err != nil { + log.Fatalf("Setup failed: %v", err) + } + return + } + + // Check if setup is needed + if setup.NeedsSetup() { + // Check if auto-setup is enabled (for Docker deployment) + if setup.AutoSetupEnabled() { + log.Println("Auto setup mode enabled...") + if err := setup.AutoSetupFromEnv(); err != nil { + log.Fatalf("Auto setup failed: %v", err) + } + // Continue to main server after auto-setup + } else { + log.Println("First run detected, starting setup wizard...") + runSetupServer() + return + } + } + + // Normal server mode + runMainServer() +} + +func runSetupServer() { + r := gin.New() + r.Use(gin.Recovery()) + r.Use(middleware.CORS()) + + // Register setup routes + setup.RegisterRoutes(r) + + // Serve embedded frontend if available + if web.HasEmbeddedFrontend() { + r.Use(web.ServeEmbeddedFrontend()) + } + + addr := ":8080" + log.Printf("Setup wizard available at http://localhost%s", addr) + log.Println("Complete the setup wizard to configure Sub2API") + + if err := r.Run(addr); err != nil { + log.Fatalf("Failed to start setup server: %v", err) + } +} + +func runMainServer() { + // 加载配置 + cfg, err := config.Load() + if err != nil { + log.Fatalf("Failed to load config: %v", err) + } + + // 初始化时区(类似 PHP 的 date_default_timezone_set) + if err := timezone.Init(cfg.Timezone); err != nil { + log.Fatalf("Failed to initialize timezone: %v", err) + } + + // 初始化数据库 + db, err := initDB(cfg) + if err != nil { + log.Fatalf("Failed to connect to database: %v", err) + } + + // 初始化Redis + rdb := initRedis(cfg) + + // 初始化Repository + repos := repository.NewRepositories(db) + + // 初始化Service + services := service.NewServices(repos, rdb, cfg) + + // 初始化Handler + buildInfo := handler.BuildInfo{ + Version: Version, + BuildType: BuildType, + } + handlers := handler.NewHandlers(services, repos, rdb, buildInfo) + + // 设置Gin模式 + if cfg.Server.Mode == "release" { + gin.SetMode(gin.ReleaseMode) + } + + // 创建路由 + r := gin.New() + r.Use(gin.Recovery()) + r.Use(middleware.Logger()) + r.Use(middleware.CORS()) + + // 注册路由 + registerRoutes(r, handlers, services, repos) + + // Serve embedded frontend if available + if web.HasEmbeddedFrontend() { + r.Use(web.ServeEmbeddedFrontend()) + } + + // 启动服务器 + srv := &http.Server{ + Addr: cfg.Server.Address(), + Handler: r, + // ReadHeaderTimeout: 读取请求头的超时时间,防止慢速请求头攻击 + ReadHeaderTimeout: time.Duration(cfg.Server.ReadHeaderTimeout) * time.Second, + // IdleTimeout: 空闲连接超时时间,释放不活跃的连接资源 + IdleTimeout: time.Duration(cfg.Server.IdleTimeout) * time.Second, + // 注意:不设置 WriteTimeout,因为流式响应可能持续十几分钟 + // 不设置 ReadTimeout,因为大请求体可能需要较长时间读取 + } + + // 优雅关闭 + go func() { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Failed to start server: %v", err) + } + }() + + log.Printf("Server started on %s", cfg.Server.Address()) + + // 等待中断信号 + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + log.Println("Shutting down server...") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := srv.Shutdown(ctx); err != nil { + log.Fatalf("Server forced to shutdown: %v", err) + } + + log.Println("Server exited") +} + +func initDB(cfg *config.Config) (*gorm.DB, error) { + gormConfig := &gorm.Config{} + if cfg.Server.Mode == "debug" { + gormConfig.Logger = logger.Default.LogMode(logger.Info) + } + + // 使用带时区的 DSN 连接数据库 + db, err := gorm.Open(postgres.Open(cfg.Database.DSNWithTimezone(cfg.Timezone)), gormConfig) + if err != nil { + return nil, err + } + + // 自动迁移(开发环境) + if cfg.Server.Mode == "debug" { + if err := model.AutoMigrate(db); err != nil { + return nil, err + } + } + + return db, nil +} + +func initRedis(cfg *config.Config) *redis.Client { + return redis.NewClient(&redis.Options{ + Addr: cfg.Redis.Address(), + Password: cfg.Redis.Password, + DB: cfg.Redis.DB, + }) +} + +func registerRoutes(r *gin.Engine, h *handler.Handlers, s *service.Services, repos *repository.Repositories) { + // 健康检查 + r.GET("/health", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok"}) + }) + + // API v1 + v1 := r.Group("/api/v1") + { + // 公开接口 + auth := v1.Group("/auth") + { + auth.POST("/register", h.Auth.Register) + auth.POST("/login", h.Auth.Login) + auth.POST("/send-verify-code", h.Auth.SendVerifyCode) + } + + // 公开设置(无需认证) + settings := v1.Group("/settings") + { + settings.GET("/public", h.Setting.GetPublicSettings) + } + + // 需要认证的接口 + authenticated := v1.Group("") + authenticated.Use(middleware.JWTAuth(s.Auth, repos.User)) + { + // 当前用户信息 + authenticated.GET("/auth/me", h.Auth.GetCurrentUser) + + // 用户接口 + user := authenticated.Group("/user") + { + user.GET("/profile", h.User.GetProfile) + user.PUT("/password", h.User.ChangePassword) + } + + // API Key管理 + keys := authenticated.Group("/keys") + { + keys.GET("", h.APIKey.List) + keys.GET("/:id", h.APIKey.GetByID) + keys.POST("", h.APIKey.Create) + keys.PUT("/:id", h.APIKey.Update) + keys.DELETE("/:id", h.APIKey.Delete) + } + + // 用户可用分组(非管理员接口) + groups := authenticated.Group("/groups") + { + groups.GET("/available", h.APIKey.GetAvailableGroups) + } + + // 使用记录 + usage := authenticated.Group("/usage") + { + usage.GET("", h.Usage.List) + usage.GET("/:id", h.Usage.GetByID) + usage.GET("/stats", h.Usage.Stats) + // User dashboard endpoints + usage.GET("/dashboard/stats", h.Usage.DashboardStats) + usage.GET("/dashboard/trend", h.Usage.DashboardTrend) + usage.GET("/dashboard/models", h.Usage.DashboardModels) + usage.POST("/dashboard/api-keys-usage", h.Usage.DashboardApiKeysUsage) + } + + // 卡密兑换 + redeem := authenticated.Group("/redeem") + { + redeem.POST("", h.Redeem.Redeem) + redeem.GET("/history", h.Redeem.GetHistory) + } + + // 用户订阅 + subscriptions := authenticated.Group("/subscriptions") + { + subscriptions.GET("", h.Subscription.List) + subscriptions.GET("/active", h.Subscription.GetActive) + subscriptions.GET("/progress", h.Subscription.GetProgress) + subscriptions.GET("/summary", h.Subscription.GetSummary) + } + } + + // 管理员接口 + admin := v1.Group("/admin") + admin.Use(middleware.JWTAuth(s.Auth, repos.User), middleware.AdminOnly()) + { + // 仪表盘 + dashboard := admin.Group("/dashboard") + { + dashboard.GET("/stats", h.Admin.Dashboard.GetStats) + dashboard.GET("/realtime", h.Admin.Dashboard.GetRealtimeMetrics) + dashboard.GET("/trend", h.Admin.Dashboard.GetUsageTrend) + dashboard.GET("/models", h.Admin.Dashboard.GetModelStats) + dashboard.GET("/api-keys-trend", h.Admin.Dashboard.GetApiKeyUsageTrend) + dashboard.GET("/users-trend", h.Admin.Dashboard.GetUserUsageTrend) + dashboard.POST("/users-usage", h.Admin.Dashboard.GetBatchUsersUsage) + dashboard.POST("/api-keys-usage", h.Admin.Dashboard.GetBatchApiKeysUsage) + } + + // 用户管理 + users := admin.Group("/users") + { + users.GET("", h.Admin.User.List) + users.GET("/:id", h.Admin.User.GetByID) + users.POST("", h.Admin.User.Create) + users.PUT("/:id", h.Admin.User.Update) + users.DELETE("/:id", h.Admin.User.Delete) + users.POST("/:id/balance", h.Admin.User.UpdateBalance) + users.GET("/:id/api-keys", h.Admin.User.GetUserAPIKeys) + users.GET("/:id/usage", h.Admin.User.GetUserUsage) + } + + // 分组管理 + groups := admin.Group("/groups") + { + groups.GET("", h.Admin.Group.List) + groups.GET("/all", h.Admin.Group.GetAll) + groups.GET("/:id", h.Admin.Group.GetByID) + groups.POST("", h.Admin.Group.Create) + groups.PUT("/:id", h.Admin.Group.Update) + groups.DELETE("/:id", h.Admin.Group.Delete) + groups.GET("/:id/stats", h.Admin.Group.GetStats) + groups.GET("/:id/api-keys", h.Admin.Group.GetGroupAPIKeys) + } + + // 账号管理 + accounts := admin.Group("/accounts") + { + accounts.GET("", h.Admin.Account.List) + accounts.GET("/:id", h.Admin.Account.GetByID) + accounts.POST("", h.Admin.Account.Create) + accounts.PUT("/:id", h.Admin.Account.Update) + accounts.DELETE("/:id", h.Admin.Account.Delete) + accounts.POST("/:id/test", h.Admin.Account.Test) + accounts.POST("/:id/refresh", h.Admin.Account.Refresh) + accounts.GET("/:id/stats", h.Admin.Account.GetStats) + accounts.POST("/:id/clear-error", h.Admin.Account.ClearError) + accounts.GET("/:id/usage", h.Admin.Account.GetUsage) + accounts.GET("/:id/today-stats", h.Admin.Account.GetTodayStats) + accounts.POST("/:id/clear-rate-limit", h.Admin.Account.ClearRateLimit) + accounts.POST("/:id/schedulable", h.Admin.Account.SetSchedulable) + accounts.POST("/batch", h.Admin.Account.BatchCreate) + + // OAuth routes + accounts.POST("/generate-auth-url", h.Admin.OAuth.GenerateAuthURL) + accounts.POST("/generate-setup-token-url", h.Admin.OAuth.GenerateSetupTokenURL) + accounts.POST("/exchange-code", h.Admin.OAuth.ExchangeCode) + accounts.POST("/exchange-setup-token-code", h.Admin.OAuth.ExchangeSetupTokenCode) + accounts.POST("/cookie-auth", h.Admin.OAuth.CookieAuth) + accounts.POST("/setup-token-cookie-auth", h.Admin.OAuth.SetupTokenCookieAuth) + } + + // 代理管理 + proxies := admin.Group("/proxies") + { + proxies.GET("", h.Admin.Proxy.List) + proxies.GET("/all", h.Admin.Proxy.GetAll) + proxies.GET("/:id", h.Admin.Proxy.GetByID) + proxies.POST("", h.Admin.Proxy.Create) + proxies.PUT("/:id", h.Admin.Proxy.Update) + proxies.DELETE("/:id", h.Admin.Proxy.Delete) + proxies.POST("/:id/test", h.Admin.Proxy.Test) + proxies.GET("/:id/stats", h.Admin.Proxy.GetStats) + proxies.GET("/:id/accounts", h.Admin.Proxy.GetProxyAccounts) + proxies.POST("/batch", h.Admin.Proxy.BatchCreate) + } + + // 卡密管理 + codes := admin.Group("/redeem-codes") + { + codes.GET("", h.Admin.Redeem.List) + codes.GET("/stats", h.Admin.Redeem.GetStats) + codes.GET("/export", h.Admin.Redeem.Export) + codes.GET("/:id", h.Admin.Redeem.GetByID) + codes.POST("/generate", h.Admin.Redeem.Generate) + codes.DELETE("/:id", h.Admin.Redeem.Delete) + codes.POST("/batch-delete", h.Admin.Redeem.BatchDelete) + codes.POST("/:id/expire", h.Admin.Redeem.Expire) + } + + // 系统设置 + adminSettings := admin.Group("/settings") + { + adminSettings.GET("", h.Admin.Setting.GetSettings) + adminSettings.PUT("", h.Admin.Setting.UpdateSettings) + adminSettings.POST("/test-smtp", h.Admin.Setting.TestSmtpConnection) + adminSettings.POST("/send-test-email", h.Admin.Setting.SendTestEmail) + } + + // 系统管理 + system := admin.Group("/system") + { + system.GET("/version", h.Admin.System.GetVersion) + system.GET("/check-updates", h.Admin.System.CheckUpdates) + system.POST("/update", h.Admin.System.PerformUpdate) + system.POST("/rollback", h.Admin.System.Rollback) + system.POST("/restart", h.Admin.System.RestartService) + } + + // 订阅管理 + subscriptions := admin.Group("/subscriptions") + { + subscriptions.GET("", h.Admin.Subscription.List) + subscriptions.GET("/:id", h.Admin.Subscription.GetByID) + subscriptions.GET("/:id/progress", h.Admin.Subscription.GetProgress) + subscriptions.POST("/assign", h.Admin.Subscription.Assign) + subscriptions.POST("/bulk-assign", h.Admin.Subscription.BulkAssign) + subscriptions.POST("/:id/extend", h.Admin.Subscription.Extend) + subscriptions.DELETE("/:id", h.Admin.Subscription.Revoke) + } + + // 分组下的订阅列表 + admin.GET("/groups/:id/subscriptions", h.Admin.Subscription.ListByGroup) + + // 用户下的订阅列表 + admin.GET("/users/:id/subscriptions", h.Admin.Subscription.ListByUser) + + // 使用记录管理 + usage := admin.Group("/usage") + { + usage.GET("", h.Admin.Usage.List) + usage.GET("/stats", h.Admin.Usage.Stats) + usage.GET("/search-users", h.Admin.Usage.SearchUsers) + usage.GET("/search-api-keys", h.Admin.Usage.SearchApiKeys) + } + } + } + + // API网关(Claude API兼容) + gateway := r.Group("/v1") + gateway.Use(middleware.ApiKeyAuthWithSubscription(s.ApiKey, s.Subscription)) + { + gateway.POST("/messages", h.Gateway.Messages) + gateway.GET("/models", h.Gateway.Models) + gateway.GET("/usage", h.Gateway.Usage) + } +} diff --git a/backend/config.yaml b/backend/config.yaml new file mode 100644 index 00000000..ff2a8920 --- /dev/null +++ b/backend/config.yaml @@ -0,0 +1,38 @@ +server: + host: "0.0.0.0" + port: 8080 + mode: "debug" # debug/release + +database: + host: "127.0.0.1" + port: 5432 + user: "postgres" + password: "XZeRr7nkjHWhm8fw" + dbname: "sub2api" + sslmode: "disable" + +redis: + host: "127.0.0.1" + port: 6379 + password: "" + db: 0 + +jwt: + secret: "your-secret-key-change-in-production" + expire_hour: 24 + +default: + admin_email: "admin@sub2api.com" + admin_password: "admin123" + user_concurrency: 5 + user_balance: 0 + api_key_prefix: "sk-" + rate_multiplier: 1.0 + +# Timezone configuration (similar to PHP's date_default_timezone_set) +# This affects ALL time operations: +# - Database timestamps +# - Usage statistics "today" boundary +# - Subscription expiry times +# Common values: Asia/Shanghai, America/New_York, Europe/London, UTC +timezone: "Asia/Shanghai" diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 00000000..b17beedf --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,74 @@ +module sub2api + +go 1.24.0 + +toolchain go1.24.11 + +require ( + github.com/gin-gonic/gin v1.9.1 + github.com/golang-jwt/jwt/v5 v5.2.0 + github.com/google/uuid v1.6.0 + github.com/imroc/req/v3 v3.56.0 + github.com/lib/pq v1.10.9 + github.com/redis/go-redis/v9 v9.3.0 + github.com/spf13/viper v1.18.2 + golang.org/x/crypto v0.44.0 + golang.org/x/term v0.37.0 + gopkg.in/yaml.v3 v3.0.1 + gorm.io/driver/postgres v1.5.4 + gorm.io/gorm v1.25.5 +) + +require ( + github.com/andybalholm/brotli v1.2.0 // indirect + github.com/bytedance/sonic v1.9.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/icholy/digest v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.4.3 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.1 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.56.0 // indirect + github.com/refraction-networking/utls v1.8.1 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 00000000..b3febba6 --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,187 @@ +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4= +github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y= +github.com/imroc/req/v3 v3.56.0 h1:t6YdqqerYBXhZ9+VjqsQs5wlKxdUNEvsgBhxWc1AEEo= +github.com/imroc/req/v3 v3.56.0/go.mod h1:cUZSooE8hhzFNOrAbdxuemXDQxFXLQTnu3066jr7ZGk= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= +github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY= +github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c= +github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0= +github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo= +github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= +golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo= +gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0= +gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= +gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go new file mode 100644 index 00000000..b4120201 --- /dev/null +++ b/backend/internal/config/config.go @@ -0,0 +1,205 @@ +package config + +import ( + "fmt" + "strings" + + "github.com/spf13/viper" +) + +type Config struct { + Server ServerConfig `mapstructure:"server"` + Database DatabaseConfig `mapstructure:"database"` + Redis RedisConfig `mapstructure:"redis"` + JWT JWTConfig `mapstructure:"jwt"` + Default DefaultConfig `mapstructure:"default"` + RateLimit RateLimitConfig `mapstructure:"rate_limit"` + Pricing PricingConfig `mapstructure:"pricing"` + Gateway GatewayConfig `mapstructure:"gateway"` + Timezone string `mapstructure:"timezone"` // e.g. "Asia/Shanghai", "UTC" +} + +type PricingConfig struct { + // 价格数据远程URL(默认使用LiteLLM镜像) + RemoteURL string `mapstructure:"remote_url"` + // 哈希校验文件URL + HashURL string `mapstructure:"hash_url"` + // 本地数据目录 + DataDir string `mapstructure:"data_dir"` + // 回退文件路径 + FallbackFile string `mapstructure:"fallback_file"` + // 更新间隔(小时) + UpdateIntervalHours int `mapstructure:"update_interval_hours"` + // 哈希校验间隔(分钟) + HashCheckIntervalMinutes int `mapstructure:"hash_check_interval_minutes"` +} + +type ServerConfig struct { + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + Mode string `mapstructure:"mode"` // debug/release + ReadHeaderTimeout int `mapstructure:"read_header_timeout"` // 读取请求头超时(秒) + IdleTimeout int `mapstructure:"idle_timeout"` // 空闲连接超时(秒) +} + +// GatewayConfig API网关相关配置 +type GatewayConfig struct { + // 等待上游响应头的超时时间(秒),0表示无超时 + // 注意:这不影响流式数据传输,只控制等待响应头的时间 + ResponseHeaderTimeout int `mapstructure:"response_header_timeout"` +} + +func (s *ServerConfig) Address() string { + return fmt.Sprintf("%s:%d", s.Host, s.Port) +} + +type DatabaseConfig struct { + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + User string `mapstructure:"user"` + Password string `mapstructure:"password"` + DBName string `mapstructure:"dbname"` + SSLMode string `mapstructure:"sslmode"` +} + +func (d *DatabaseConfig) DSN() string { + return fmt.Sprintf( + "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", + d.Host, d.Port, d.User, d.Password, d.DBName, d.SSLMode, + ) +} + +// DSNWithTimezone returns DSN with timezone setting +func (d *DatabaseConfig) DSNWithTimezone(tz string) string { + if tz == "" { + tz = "Asia/Shanghai" + } + return fmt.Sprintf( + "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s TimeZone=%s", + d.Host, d.Port, d.User, d.Password, d.DBName, d.SSLMode, tz, + ) +} + +type RedisConfig struct { + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + Password string `mapstructure:"password"` + DB int `mapstructure:"db"` +} + +func (r *RedisConfig) Address() string { + return fmt.Sprintf("%s:%d", r.Host, r.Port) +} + +type JWTConfig struct { + Secret string `mapstructure:"secret"` + ExpireHour int `mapstructure:"expire_hour"` +} + +type DefaultConfig struct { + AdminEmail string `mapstructure:"admin_email"` + AdminPassword string `mapstructure:"admin_password"` + UserConcurrency int `mapstructure:"user_concurrency"` + UserBalance float64 `mapstructure:"user_balance"` + ApiKeyPrefix string `mapstructure:"api_key_prefix"` + RateMultiplier float64 `mapstructure:"rate_multiplier"` +} + +type RateLimitConfig struct { + OverloadCooldownMinutes int `mapstructure:"overload_cooldown_minutes"` // 529过载冷却时间(分钟) +} + +func Load() (*Config, error) { + viper.SetConfigName("config") + viper.SetConfigType("yaml") + viper.AddConfigPath(".") + viper.AddConfigPath("./config") + viper.AddConfigPath("/etc/sub2api") + + // 环境变量支持 + viper.AutomaticEnv() + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + + // 默认值 + setDefaults() + + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); !ok { + return nil, fmt.Errorf("read config error: %w", err) + } + // 配置文件不存在时使用默认值 + } + + var cfg Config + if err := viper.Unmarshal(&cfg); err != nil { + return nil, fmt.Errorf("unmarshal config error: %w", err) + } + + if err := cfg.Validate(); err != nil { + return nil, fmt.Errorf("validate config error: %w", err) + } + + return &cfg, nil +} + +func setDefaults() { + // Server + viper.SetDefault("server.host", "0.0.0.0") + viper.SetDefault("server.port", 8080) + viper.SetDefault("server.mode", "debug") + viper.SetDefault("server.read_header_timeout", 30) // 30秒读取请求头 + viper.SetDefault("server.idle_timeout", 120) // 120秒空闲超时 + + // Database + viper.SetDefault("database.host", "localhost") + viper.SetDefault("database.port", 5432) + viper.SetDefault("database.user", "postgres") + viper.SetDefault("database.password", "postgres") + viper.SetDefault("database.dbname", "sub2api") + viper.SetDefault("database.sslmode", "disable") + + // Redis + viper.SetDefault("redis.host", "localhost") + viper.SetDefault("redis.port", 6379) + viper.SetDefault("redis.password", "") + viper.SetDefault("redis.db", 0) + + // JWT + viper.SetDefault("jwt.secret", "change-me-in-production") + viper.SetDefault("jwt.expire_hour", 24) + + // Default + viper.SetDefault("default.admin_email", "admin@sub2api.com") + viper.SetDefault("default.admin_password", "admin123") + viper.SetDefault("default.user_concurrency", 5) + viper.SetDefault("default.user_balance", 0) + viper.SetDefault("default.api_key_prefix", "sk-") + viper.SetDefault("default.rate_multiplier", 1.0) + + // RateLimit + viper.SetDefault("rate_limit.overload_cooldown_minutes", 10) + + // Pricing - 从 price-mirror 分支同步,该分支维护了 sha256 哈希文件用于增量更新检查 + viper.SetDefault("pricing.remote_url", "https://raw.githubusercontent.com/Wei-Shaw/claude-relay-service/price-mirror/model_prices_and_context_window.json") + viper.SetDefault("pricing.hash_url", "https://raw.githubusercontent.com/Wei-Shaw/claude-relay-service/price-mirror/model_prices_and_context_window.sha256") + viper.SetDefault("pricing.data_dir", "./data") + viper.SetDefault("pricing.fallback_file", "./resources/model-pricing/model_prices_and_context_window.json") + viper.SetDefault("pricing.update_interval_hours", 24) + viper.SetDefault("pricing.hash_check_interval_minutes", 10) + + // Timezone (default to Asia/Shanghai for Chinese users) + viper.SetDefault("timezone", "Asia/Shanghai") + + // Gateway + viper.SetDefault("gateway.response_header_timeout", 300) // 300秒(5分钟)等待上游响应头,LLM高负载时可能排队较久 +} + +func (c *Config) Validate() error { + if c.JWT.Secret == "" { + return fmt.Errorf("jwt.secret is required") + } + if c.JWT.Secret == "change-me-in-production" && c.Server.Mode == "release" { + return fmt.Errorf("jwt.secret must be changed in production") + } + return nil +} diff --git a/backend/internal/handler/admin/account_handler.go b/backend/internal/handler/admin/account_handler.go new file mode 100644 index 00000000..71804873 --- /dev/null +++ b/backend/internal/handler/admin/account_handler.go @@ -0,0 +1,537 @@ +package admin + +import ( + "strconv" + + "sub2api/internal/pkg/response" + "sub2api/internal/service" + + "github.com/gin-gonic/gin" +) + +// OAuthHandler handles OAuth-related operations for accounts +type OAuthHandler struct { + oauthService *service.OAuthService + adminService service.AdminService +} + +// NewOAuthHandler creates a new OAuth handler +func NewOAuthHandler(oauthService *service.OAuthService, adminService service.AdminService) *OAuthHandler { + return &OAuthHandler{ + oauthService: oauthService, + adminService: adminService, + } +} + +// AccountHandler handles admin account management +type AccountHandler struct { + adminService service.AdminService + oauthService *service.OAuthService + rateLimitService *service.RateLimitService + accountUsageService *service.AccountUsageService + accountTestService *service.AccountTestService +} + +// NewAccountHandler creates a new admin account handler +func NewAccountHandler(adminService service.AdminService, oauthService *service.OAuthService, rateLimitService *service.RateLimitService, accountUsageService *service.AccountUsageService, accountTestService *service.AccountTestService) *AccountHandler { + return &AccountHandler{ + adminService: adminService, + oauthService: oauthService, + rateLimitService: rateLimitService, + accountUsageService: accountUsageService, + accountTestService: accountTestService, + } +} + +// CreateAccountRequest represents create account request +type CreateAccountRequest struct { + Name string `json:"name" binding:"required"` + Platform string `json:"platform" binding:"required"` + Type string `json:"type" binding:"required,oneof=oauth setup-token apikey"` + Credentials map[string]interface{} `json:"credentials" binding:"required"` + Extra map[string]interface{} `json:"extra"` + ProxyID *int64 `json:"proxy_id"` + Concurrency int `json:"concurrency"` + Priority int `json:"priority"` + GroupIDs []int64 `json:"group_ids"` +} + +// UpdateAccountRequest represents update account request +// 使用指针类型来区分"未提供"和"设置为0" +type UpdateAccountRequest struct { + Name string `json:"name"` + Type string `json:"type" binding:"omitempty,oneof=oauth setup-token apikey"` + Credentials map[string]interface{} `json:"credentials"` + Extra map[string]interface{} `json:"extra"` + ProxyID *int64 `json:"proxy_id"` + Concurrency *int `json:"concurrency"` + Priority *int `json:"priority"` + Status string `json:"status" binding:"omitempty,oneof=active inactive"` + GroupIDs *[]int64 `json:"group_ids"` +} + +// List handles listing all accounts with pagination +// GET /api/v1/admin/accounts +func (h *AccountHandler) List(c *gin.Context) { + page, pageSize := response.ParsePagination(c) + platform := c.Query("platform") + accountType := c.Query("type") + status := c.Query("status") + search := c.Query("search") + + accounts, total, err := h.adminService.ListAccounts(c.Request.Context(), page, pageSize, platform, accountType, status, search) + if err != nil { + response.InternalError(c, "Failed to list accounts: "+err.Error()) + return + } + + response.Paginated(c, accounts, total, page, pageSize) +} + +// GetByID handles getting an account by ID +// GET /api/v1/admin/accounts/:id +func (h *AccountHandler) GetByID(c *gin.Context) { + accountID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid account ID") + return + } + + account, err := h.adminService.GetAccount(c.Request.Context(), accountID) + if err != nil { + response.NotFound(c, "Account not found") + return + } + + response.Success(c, account) +} + +// Create handles creating a new account +// POST /api/v1/admin/accounts +func (h *AccountHandler) Create(c *gin.Context) { + var req CreateAccountRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + account, err := h.adminService.CreateAccount(c.Request.Context(), &service.CreateAccountInput{ + Name: req.Name, + Platform: req.Platform, + Type: req.Type, + Credentials: req.Credentials, + Extra: req.Extra, + ProxyID: req.ProxyID, + Concurrency: req.Concurrency, + Priority: req.Priority, + GroupIDs: req.GroupIDs, + }) + if err != nil { + response.BadRequest(c, "Failed to create account: "+err.Error()) + return + } + + response.Success(c, account) +} + +// Update handles updating an account +// PUT /api/v1/admin/accounts/:id +func (h *AccountHandler) Update(c *gin.Context) { + accountID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid account ID") + return + } + + var req UpdateAccountRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + account, err := h.adminService.UpdateAccount(c.Request.Context(), accountID, &service.UpdateAccountInput{ + Name: req.Name, + Type: req.Type, + Credentials: req.Credentials, + Extra: req.Extra, + ProxyID: req.ProxyID, + Concurrency: req.Concurrency, // 指针类型,nil 表示未提供 + Priority: req.Priority, // 指针类型,nil 表示未提供 + Status: req.Status, + GroupIDs: req.GroupIDs, + }) + if err != nil { + response.InternalError(c, "Failed to update account: "+err.Error()) + return + } + + response.Success(c, account) +} + +// Delete handles deleting an account +// DELETE /api/v1/admin/accounts/:id +func (h *AccountHandler) Delete(c *gin.Context) { + accountID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid account ID") + return + } + + err = h.adminService.DeleteAccount(c.Request.Context(), accountID) + if err != nil { + response.InternalError(c, "Failed to delete account: "+err.Error()) + return + } + + response.Success(c, gin.H{"message": "Account deleted successfully"}) +} + +// Test handles testing account connectivity with SSE streaming +// POST /api/v1/admin/accounts/:id/test +func (h *AccountHandler) Test(c *gin.Context) { + accountID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid account ID") + return + } + + // Use AccountTestService to test the account with SSE streaming + if err := h.accountTestService.TestAccountConnection(c, accountID); err != nil { + // Error already sent via SSE, just log + return + } +} + +// Refresh handles refreshing account credentials +// POST /api/v1/admin/accounts/:id/refresh +func (h *AccountHandler) Refresh(c *gin.Context) { + accountID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid account ID") + return + } + + // Get account + account, err := h.adminService.GetAccount(c.Request.Context(), accountID) + if err != nil { + response.NotFound(c, "Account not found") + return + } + + // Only refresh OAuth-based accounts (oauth and setup-token) + if !account.IsOAuth() { + response.BadRequest(c, "Cannot refresh non-OAuth account credentials") + return + } + + // Use OAuth service to refresh token + tokenInfo, err := h.oauthService.RefreshAccountToken(c.Request.Context(), account) + if err != nil { + response.InternalError(c, "Failed to refresh credentials: "+err.Error()) + return + } + + // Update account credentials + newCredentials := map[string]interface{}{ + "access_token": tokenInfo.AccessToken, + "token_type": tokenInfo.TokenType, + "expires_in": tokenInfo.ExpiresIn, + "expires_at": tokenInfo.ExpiresAt, + "refresh_token": tokenInfo.RefreshToken, + "scope": tokenInfo.Scope, + } + + updatedAccount, err := h.adminService.UpdateAccount(c.Request.Context(), accountID, &service.UpdateAccountInput{ + Credentials: newCredentials, + }) + if err != nil { + response.InternalError(c, "Failed to update account credentials: "+err.Error()) + return + } + + response.Success(c, updatedAccount) +} + +// GetStats handles getting account statistics +// GET /api/v1/admin/accounts/:id/stats +func (h *AccountHandler) GetStats(c *gin.Context) { + accountID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid account ID") + return + } + + // Return mock data for now + _ = accountID + response.Success(c, gin.H{ + "total_requests": 0, + "successful_requests": 0, + "failed_requests": 0, + "total_tokens": 0, + "average_response_time": 0, + }) +} + +// ClearError handles clearing account error +// POST /api/v1/admin/accounts/:id/clear-error +func (h *AccountHandler) ClearError(c *gin.Context) { + accountID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid account ID") + return + } + + account, err := h.adminService.ClearAccountError(c.Request.Context(), accountID) + if err != nil { + response.InternalError(c, "Failed to clear error: "+err.Error()) + return + } + + response.Success(c, account) +} + +// BatchCreate handles batch creating accounts +// POST /api/v1/admin/accounts/batch +func (h *AccountHandler) BatchCreate(c *gin.Context) { + var req struct { + Accounts []CreateAccountRequest `json:"accounts" binding:"required,min=1"` + } + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + // Return mock data for now + response.Success(c, gin.H{ + "success": len(req.Accounts), + "failed": 0, + "results": []gin.H{}, + }) +} + +// ========== OAuth Handlers ========== + +// GenerateAuthURLRequest represents the request for generating auth URL +type GenerateAuthURLRequest struct { + ProxyID *int64 `json:"proxy_id"` +} + +// GenerateAuthURL generates OAuth authorization URL with full scope +// POST /api/v1/admin/accounts/generate-auth-url +func (h *OAuthHandler) GenerateAuthURL(c *gin.Context) { + var req GenerateAuthURLRequest + if err := c.ShouldBindJSON(&req); err != nil { + // Allow empty body + req = GenerateAuthURLRequest{} + } + + result, err := h.oauthService.GenerateAuthURL(c.Request.Context(), req.ProxyID) + if err != nil { + response.InternalError(c, "Failed to generate auth URL: "+err.Error()) + return + } + + response.Success(c, result) +} + +// GenerateSetupTokenURL generates OAuth authorization URL for setup token (inference only) +// POST /api/v1/admin/accounts/generate-setup-token-url +func (h *OAuthHandler) GenerateSetupTokenURL(c *gin.Context) { + var req GenerateAuthURLRequest + if err := c.ShouldBindJSON(&req); err != nil { + // Allow empty body + req = GenerateAuthURLRequest{} + } + + result, err := h.oauthService.GenerateSetupTokenURL(c.Request.Context(), req.ProxyID) + if err != nil { + response.InternalError(c, "Failed to generate setup token URL: "+err.Error()) + return + } + + response.Success(c, result) +} + +// ExchangeCodeRequest represents the request for exchanging auth code +type ExchangeCodeRequest struct { + SessionID string `json:"session_id" binding:"required"` + Code string `json:"code" binding:"required"` + ProxyID *int64 `json:"proxy_id"` +} + +// ExchangeCode exchanges authorization code for tokens +// POST /api/v1/admin/accounts/exchange-code +func (h *OAuthHandler) ExchangeCode(c *gin.Context) { + var req ExchangeCodeRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + tokenInfo, err := h.oauthService.ExchangeCode(c.Request.Context(), &service.ExchangeCodeInput{ + SessionID: req.SessionID, + Code: req.Code, + ProxyID: req.ProxyID, + }) + if err != nil { + response.BadRequest(c, "Failed to exchange code: "+err.Error()) + return + } + + response.Success(c, tokenInfo) +} + +// ExchangeSetupTokenCode exchanges authorization code for setup token +// POST /api/v1/admin/accounts/exchange-setup-token-code +func (h *OAuthHandler) ExchangeSetupTokenCode(c *gin.Context) { + var req ExchangeCodeRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + tokenInfo, err := h.oauthService.ExchangeCode(c.Request.Context(), &service.ExchangeCodeInput{ + SessionID: req.SessionID, + Code: req.Code, + ProxyID: req.ProxyID, + }) + if err != nil { + response.BadRequest(c, "Failed to exchange code: "+err.Error()) + return + } + + response.Success(c, tokenInfo) +} + +// CookieAuthRequest represents the request for cookie-based authentication +type CookieAuthRequest struct { + SessionKey string `json:"code" binding:"required"` // Using 'code' field as sessionKey (frontend sends it this way) + ProxyID *int64 `json:"proxy_id"` +} + +// CookieAuth performs OAuth using sessionKey (cookie-based auto-auth) +// POST /api/v1/admin/accounts/cookie-auth +func (h *OAuthHandler) CookieAuth(c *gin.Context) { + var req CookieAuthRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + tokenInfo, err := h.oauthService.CookieAuth(c.Request.Context(), &service.CookieAuthInput{ + SessionKey: req.SessionKey, + ProxyID: req.ProxyID, + Scope: "full", + }) + if err != nil { + response.BadRequest(c, "Cookie auth failed: "+err.Error()) + return + } + + response.Success(c, tokenInfo) +} + +// SetupTokenCookieAuth performs OAuth using sessionKey for setup token (inference only) +// POST /api/v1/admin/accounts/setup-token-cookie-auth +func (h *OAuthHandler) SetupTokenCookieAuth(c *gin.Context) { + var req CookieAuthRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + tokenInfo, err := h.oauthService.CookieAuth(c.Request.Context(), &service.CookieAuthInput{ + SessionKey: req.SessionKey, + ProxyID: req.ProxyID, + Scope: "inference", + }) + if err != nil { + response.BadRequest(c, "Cookie auth failed: "+err.Error()) + return + } + + response.Success(c, tokenInfo) +} + +// GetUsage handles getting account usage information +// GET /api/v1/admin/accounts/:id/usage +func (h *AccountHandler) GetUsage(c *gin.Context) { + accountID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid account ID") + return + } + + usage, err := h.accountUsageService.GetUsage(c.Request.Context(), accountID) + if err != nil { + response.InternalError(c, "Failed to get usage: "+err.Error()) + return + } + + response.Success(c, usage) +} + +// ClearRateLimit handles clearing account rate limit status +// POST /api/v1/admin/accounts/:id/clear-rate-limit +func (h *AccountHandler) ClearRateLimit(c *gin.Context) { + accountID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid account ID") + return + } + + err = h.rateLimitService.ClearRateLimit(c.Request.Context(), accountID) + if err != nil { + response.InternalError(c, "Failed to clear rate limit: "+err.Error()) + return + } + + response.Success(c, gin.H{"message": "Rate limit cleared successfully"}) +} + +// GetTodayStats handles getting account today statistics +// GET /api/v1/admin/accounts/:id/today-stats +func (h *AccountHandler) GetTodayStats(c *gin.Context) { + accountID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid account ID") + return + } + + stats, err := h.accountUsageService.GetTodayStats(c.Request.Context(), accountID) + if err != nil { + response.InternalError(c, "Failed to get today stats: "+err.Error()) + return + } + + response.Success(c, stats) +} + +// SetSchedulableRequest represents the request body for setting schedulable status +type SetSchedulableRequest struct { + Schedulable bool `json:"schedulable"` +} + +// SetSchedulable handles toggling account schedulable status +// POST /api/v1/admin/accounts/:id/schedulable +func (h *AccountHandler) SetSchedulable(c *gin.Context) { + accountID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid account ID") + return + } + + var req SetSchedulableRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + account, err := h.adminService.SetAccountSchedulable(c.Request.Context(), accountID, req.Schedulable) + if err != nil { + response.InternalError(c, "Failed to update schedulable status: "+err.Error()) + return + } + + response.Success(c, account) +} diff --git a/backend/internal/handler/admin/dashboard_handler.go b/backend/internal/handler/admin/dashboard_handler.go new file mode 100644 index 00000000..3b971eab --- /dev/null +++ b/backend/internal/handler/admin/dashboard_handler.go @@ -0,0 +1,274 @@ +package admin + +import ( + "strconv" + "sub2api/internal/pkg/response" + "sub2api/internal/pkg/timezone" + "sub2api/internal/repository" + "sub2api/internal/service" + "time" + + "github.com/gin-gonic/gin" +) + +// DashboardHandler handles admin dashboard statistics +type DashboardHandler struct { + adminService service.AdminService + usageRepo *repository.UsageLogRepository + startTime time.Time // Server start time for uptime calculation +} + +// NewDashboardHandler creates a new admin dashboard handler +func NewDashboardHandler(adminService service.AdminService, usageRepo *repository.UsageLogRepository) *DashboardHandler { + return &DashboardHandler{ + adminService: adminService, + usageRepo: usageRepo, + startTime: time.Now(), + } +} + +// parseTimeRange parses start_date, end_date query parameters +func parseTimeRange(c *gin.Context) (time.Time, time.Time) { + now := timezone.Now() + startDate := c.Query("start_date") + endDate := c.Query("end_date") + + var startTime, endTime time.Time + + if startDate != "" { + if t, err := timezone.ParseInLocation("2006-01-02", startDate); err == nil { + startTime = t + } else { + startTime = timezone.StartOfDay(now.AddDate(0, 0, -7)) + } + } else { + startTime = timezone.StartOfDay(now.AddDate(0, 0, -7)) + } + + if endDate != "" { + if t, err := timezone.ParseInLocation("2006-01-02", endDate); err == nil { + endTime = t.Add(24 * time.Hour) // Include the end date + } else { + endTime = timezone.StartOfDay(now.AddDate(0, 0, 1)) + } + } else { + endTime = timezone.StartOfDay(now.AddDate(0, 0, 1)) + } + + return startTime, endTime +} + +// GetStats handles getting dashboard statistics +// GET /api/v1/admin/dashboard/stats +func (h *DashboardHandler) GetStats(c *gin.Context) { + stats, err := h.usageRepo.GetDashboardStats(c.Request.Context()) + if err != nil { + response.Error(c, 500, "Failed to get dashboard statistics") + return + } + + // Calculate uptime in seconds + uptime := int64(time.Since(h.startTime).Seconds()) + + response.Success(c, gin.H{ + // 用户统计 + "total_users": stats.TotalUsers, + "today_new_users": stats.TodayNewUsers, + "active_users": stats.ActiveUsers, + + // API Key 统计 + "total_api_keys": stats.TotalApiKeys, + "active_api_keys": stats.ActiveApiKeys, + + // 账户统计 + "total_accounts": stats.TotalAccounts, + "normal_accounts": stats.NormalAccounts, + "error_accounts": stats.ErrorAccounts, + "ratelimit_accounts": stats.RateLimitAccounts, + "overload_accounts": stats.OverloadAccounts, + + // 累计 Token 使用统计 + "total_requests": stats.TotalRequests, + "total_input_tokens": stats.TotalInputTokens, + "total_output_tokens": stats.TotalOutputTokens, + "total_cache_creation_tokens": stats.TotalCacheCreationTokens, + "total_cache_read_tokens": stats.TotalCacheReadTokens, + "total_tokens": stats.TotalTokens, + "total_cost": stats.TotalCost, // 标准计费 + "total_actual_cost": stats.TotalActualCost, // 实际扣除 + + // 今日 Token 使用统计 + "today_requests": stats.TodayRequests, + "today_input_tokens": stats.TodayInputTokens, + "today_output_tokens": stats.TodayOutputTokens, + "today_cache_creation_tokens": stats.TodayCacheCreationTokens, + "today_cache_read_tokens": stats.TodayCacheReadTokens, + "today_tokens": stats.TodayTokens, + "today_cost": stats.TodayCost, // 今日标准计费 + "today_actual_cost": stats.TodayActualCost, // 今日实际扣除 + + // 系统运行统计 + "average_duration_ms": stats.AverageDurationMs, + "uptime": uptime, + }) +} + +// GetRealtimeMetrics handles getting real-time system metrics +// GET /api/v1/admin/dashboard/realtime +func (h *DashboardHandler) GetRealtimeMetrics(c *gin.Context) { + // Return mock data for now + response.Success(c, gin.H{ + "active_requests": 0, + "requests_per_minute": 0, + "average_response_time": 0, + "error_rate": 0.0, + }) +} + +// GetUsageTrend handles getting usage trend data +// GET /api/v1/admin/dashboard/trend +// Query params: start_date, end_date (YYYY-MM-DD), granularity (day/hour) +func (h *DashboardHandler) GetUsageTrend(c *gin.Context) { + startTime, endTime := parseTimeRange(c) + granularity := c.DefaultQuery("granularity", "day") + + trend, err := h.usageRepo.GetUsageTrend(c.Request.Context(), startTime, endTime, granularity) + if err != nil { + response.Error(c, 500, "Failed to get usage trend") + return + } + + response.Success(c, gin.H{ + "trend": trend, + "start_date": startTime.Format("2006-01-02"), + "end_date": endTime.Add(-24 * time.Hour).Format("2006-01-02"), + "granularity": granularity, + }) +} + +// GetModelStats handles getting model usage statistics +// GET /api/v1/admin/dashboard/models +// Query params: start_date, end_date (YYYY-MM-DD) +func (h *DashboardHandler) GetModelStats(c *gin.Context) { + startTime, endTime := parseTimeRange(c) + + stats, err := h.usageRepo.GetModelStats(c.Request.Context(), startTime, endTime) + if err != nil { + response.Error(c, 500, "Failed to get model statistics") + return + } + + response.Success(c, gin.H{ + "models": stats, + "start_date": startTime.Format("2006-01-02"), + "end_date": endTime.Add(-24 * time.Hour).Format("2006-01-02"), + }) +} + +// GetApiKeyUsageTrend handles getting API key usage trend data +// GET /api/v1/admin/dashboard/api-keys-trend +// Query params: start_date, end_date (YYYY-MM-DD), granularity (day/hour), limit (default 5) +func (h *DashboardHandler) GetApiKeyUsageTrend(c *gin.Context) { + startTime, endTime := parseTimeRange(c) + granularity := c.DefaultQuery("granularity", "day") + limitStr := c.DefaultQuery("limit", "5") + limit, err := strconv.Atoi(limitStr) + if err != nil || limit <= 0 { + limit = 5 + } + + trend, err := h.usageRepo.GetApiKeyUsageTrend(c.Request.Context(), startTime, endTime, granularity, limit) + if err != nil { + response.Error(c, 500, "Failed to get API key usage trend") + return + } + + response.Success(c, gin.H{ + "trend": trend, + "start_date": startTime.Format("2006-01-02"), + "end_date": endTime.Add(-24 * time.Hour).Format("2006-01-02"), + "granularity": granularity, + }) +} + +// GetUserUsageTrend handles getting user usage trend data +// GET /api/v1/admin/dashboard/users-trend +// Query params: start_date, end_date (YYYY-MM-DD), granularity (day/hour), limit (default 12) +func (h *DashboardHandler) GetUserUsageTrend(c *gin.Context) { + startTime, endTime := parseTimeRange(c) + granularity := c.DefaultQuery("granularity", "day") + limitStr := c.DefaultQuery("limit", "12") + limit, err := strconv.Atoi(limitStr) + if err != nil || limit <= 0 { + limit = 12 + } + + trend, err := h.usageRepo.GetUserUsageTrend(c.Request.Context(), startTime, endTime, granularity, limit) + if err != nil { + response.Error(c, 500, "Failed to get user usage trend") + return + } + + response.Success(c, gin.H{ + "trend": trend, + "start_date": startTime.Format("2006-01-02"), + "end_date": endTime.Add(-24 * time.Hour).Format("2006-01-02"), + "granularity": granularity, + }) +} + +// BatchUsersUsageRequest represents the request body for batch user usage stats +type BatchUsersUsageRequest struct { + UserIDs []int64 `json:"user_ids" binding:"required"` +} + +// GetBatchUsersUsage handles getting usage stats for multiple users +// POST /api/v1/admin/dashboard/users-usage +func (h *DashboardHandler) GetBatchUsersUsage(c *gin.Context) { + var req BatchUsersUsageRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + if len(req.UserIDs) == 0 { + response.Success(c, gin.H{"stats": map[string]interface{}{}}) + return + } + + stats, err := h.usageRepo.GetBatchUserUsageStats(c.Request.Context(), req.UserIDs) + if err != nil { + response.Error(c, 500, "Failed to get user usage stats") + return + } + + response.Success(c, gin.H{"stats": stats}) +} + +// BatchApiKeysUsageRequest represents the request body for batch api key usage stats +type BatchApiKeysUsageRequest struct { + ApiKeyIDs []int64 `json:"api_key_ids" binding:"required"` +} + +// GetBatchApiKeysUsage handles getting usage stats for multiple API keys +// POST /api/v1/admin/dashboard/api-keys-usage +func (h *DashboardHandler) GetBatchApiKeysUsage(c *gin.Context) { + var req BatchApiKeysUsageRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + if len(req.ApiKeyIDs) == 0 { + response.Success(c, gin.H{"stats": map[string]interface{}{}}) + return + } + + stats, err := h.usageRepo.GetBatchApiKeyUsageStats(c.Request.Context(), req.ApiKeyIDs) + if err != nil { + response.Error(c, 500, "Failed to get API key usage stats") + return + } + + response.Success(c, gin.H{"stats": stats}) +} diff --git a/backend/internal/handler/admin/group_handler.go b/backend/internal/handler/admin/group_handler.go new file mode 100644 index 00000000..24b691d3 --- /dev/null +++ b/backend/internal/handler/admin/group_handler.go @@ -0,0 +1,233 @@ +package admin + +import ( + "strconv" + + "sub2api/internal/model" + "sub2api/internal/pkg/response" + "sub2api/internal/service" + + "github.com/gin-gonic/gin" +) + +// GroupHandler handles admin group management +type GroupHandler struct { + adminService service.AdminService +} + +// NewGroupHandler creates a new admin group handler +func NewGroupHandler(adminService service.AdminService) *GroupHandler { + return &GroupHandler{ + adminService: adminService, + } +} + +// CreateGroupRequest represents create group request +type CreateGroupRequest struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` + Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini"` + RateMultiplier float64 `json:"rate_multiplier"` + IsExclusive bool `json:"is_exclusive"` + SubscriptionType string `json:"subscription_type" binding:"omitempty,oneof=standard subscription"` + DailyLimitUSD *float64 `json:"daily_limit_usd"` + WeeklyLimitUSD *float64 `json:"weekly_limit_usd"` + MonthlyLimitUSD *float64 `json:"monthly_limit_usd"` +} + +// UpdateGroupRequest represents update group request +type UpdateGroupRequest struct { + Name string `json:"name"` + Description string `json:"description"` + Platform string `json:"platform" binding:"omitempty,oneof=anthropic openai gemini"` + RateMultiplier *float64 `json:"rate_multiplier"` + IsExclusive *bool `json:"is_exclusive"` + Status string `json:"status" binding:"omitempty,oneof=active inactive"` + SubscriptionType string `json:"subscription_type" binding:"omitempty,oneof=standard subscription"` + DailyLimitUSD *float64 `json:"daily_limit_usd"` + WeeklyLimitUSD *float64 `json:"weekly_limit_usd"` + MonthlyLimitUSD *float64 `json:"monthly_limit_usd"` +} + +// List handles listing all groups with pagination +// GET /api/v1/admin/groups +func (h *GroupHandler) List(c *gin.Context) { + page, pageSize := response.ParsePagination(c) + platform := c.Query("platform") + status := c.Query("status") + isExclusiveStr := c.Query("is_exclusive") + + var isExclusive *bool + if isExclusiveStr != "" { + val := isExclusiveStr == "true" + isExclusive = &val + } + + groups, total, err := h.adminService.ListGroups(c.Request.Context(), page, pageSize, platform, status, isExclusive) + if err != nil { + response.InternalError(c, "Failed to list groups: "+err.Error()) + return + } + + response.Paginated(c, groups, total, page, pageSize) +} + +// GetAll handles getting all active groups without pagination +// GET /api/v1/admin/groups/all +func (h *GroupHandler) GetAll(c *gin.Context) { + platform := c.Query("platform") + + var groups []model.Group + var err error + + if platform != "" { + groups, err = h.adminService.GetAllGroupsByPlatform(c.Request.Context(), platform) + } else { + groups, err = h.adminService.GetAllGroups(c.Request.Context()) + } + + if err != nil { + response.InternalError(c, "Failed to get groups: "+err.Error()) + return + } + + response.Success(c, groups) +} + +// GetByID handles getting a group by ID +// GET /api/v1/admin/groups/:id +func (h *GroupHandler) GetByID(c *gin.Context) { + groupID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid group ID") + return + } + + group, err := h.adminService.GetGroup(c.Request.Context(), groupID) + if err != nil { + response.NotFound(c, "Group not found") + return + } + + response.Success(c, group) +} + +// Create handles creating a new group +// POST /api/v1/admin/groups +func (h *GroupHandler) Create(c *gin.Context) { + var req CreateGroupRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + group, err := h.adminService.CreateGroup(c.Request.Context(), &service.CreateGroupInput{ + Name: req.Name, + Description: req.Description, + Platform: req.Platform, + RateMultiplier: req.RateMultiplier, + IsExclusive: req.IsExclusive, + SubscriptionType: req.SubscriptionType, + DailyLimitUSD: req.DailyLimitUSD, + WeeklyLimitUSD: req.WeeklyLimitUSD, + MonthlyLimitUSD: req.MonthlyLimitUSD, + }) + if err != nil { + response.BadRequest(c, "Failed to create group: "+err.Error()) + return + } + + response.Success(c, group) +} + +// Update handles updating a group +// PUT /api/v1/admin/groups/:id +func (h *GroupHandler) Update(c *gin.Context) { + groupID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid group ID") + return + } + + var req UpdateGroupRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + group, err := h.adminService.UpdateGroup(c.Request.Context(), groupID, &service.UpdateGroupInput{ + Name: req.Name, + Description: req.Description, + Platform: req.Platform, + RateMultiplier: req.RateMultiplier, + IsExclusive: req.IsExclusive, + Status: req.Status, + SubscriptionType: req.SubscriptionType, + DailyLimitUSD: req.DailyLimitUSD, + WeeklyLimitUSD: req.WeeklyLimitUSD, + MonthlyLimitUSD: req.MonthlyLimitUSD, + }) + if err != nil { + response.InternalError(c, "Failed to update group: "+err.Error()) + return + } + + response.Success(c, group) +} + +// Delete handles deleting a group +// DELETE /api/v1/admin/groups/:id +func (h *GroupHandler) Delete(c *gin.Context) { + groupID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid group ID") + return + } + + err = h.adminService.DeleteGroup(c.Request.Context(), groupID) + if err != nil { + response.InternalError(c, "Failed to delete group: "+err.Error()) + return + } + + response.Success(c, gin.H{"message": "Group deleted successfully"}) +} + +// GetStats handles getting group statistics +// GET /api/v1/admin/groups/:id/stats +func (h *GroupHandler) GetStats(c *gin.Context) { + groupID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid group ID") + return + } + + // Return mock data for now + response.Success(c, gin.H{ + "total_api_keys": 0, + "active_api_keys": 0, + "total_requests": 0, + "total_cost": 0.0, + }) + _ = groupID // TODO: implement actual stats +} + +// GetGroupAPIKeys handles getting API keys in a group +// GET /api/v1/admin/groups/:id/api-keys +func (h *GroupHandler) GetGroupAPIKeys(c *gin.Context) { + groupID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid group ID") + return + } + + page, pageSize := response.ParsePagination(c) + + keys, total, err := h.adminService.GetGroupAPIKeys(c.Request.Context(), groupID, page, pageSize) + if err != nil { + response.InternalError(c, "Failed to get group API keys: "+err.Error()) + return + } + + response.Paginated(c, keys, total, page, pageSize) +} diff --git a/backend/internal/handler/admin/proxy_handler.go b/backend/internal/handler/admin/proxy_handler.go new file mode 100644 index 00000000..944f776e --- /dev/null +++ b/backend/internal/handler/admin/proxy_handler.go @@ -0,0 +1,300 @@ +package admin + +import ( + "strconv" + + "sub2api/internal/pkg/response" + "sub2api/internal/service" + + "github.com/gin-gonic/gin" +) + +// ProxyHandler handles admin proxy management +type ProxyHandler struct { + adminService service.AdminService +} + +// NewProxyHandler creates a new admin proxy handler +func NewProxyHandler(adminService service.AdminService) *ProxyHandler { + return &ProxyHandler{ + adminService: adminService, + } +} + +// CreateProxyRequest represents create proxy request +type CreateProxyRequest struct { + Name string `json:"name" binding:"required"` + Protocol string `json:"protocol" binding:"required,oneof=http https socks5"` + Host string `json:"host" binding:"required"` + Port int `json:"port" binding:"required,min=1,max=65535"` + Username string `json:"username"` + Password string `json:"password"` +} + +// UpdateProxyRequest represents update proxy request +type UpdateProxyRequest struct { + Name string `json:"name"` + Protocol string `json:"protocol" binding:"omitempty,oneof=http https socks5"` + Host string `json:"host"` + Port int `json:"port" binding:"omitempty,min=1,max=65535"` + Username string `json:"username"` + Password string `json:"password"` + Status string `json:"status" binding:"omitempty,oneof=active inactive"` +} + +// List handles listing all proxies with pagination +// GET /api/v1/admin/proxies +func (h *ProxyHandler) List(c *gin.Context) { + page, pageSize := response.ParsePagination(c) + protocol := c.Query("protocol") + status := c.Query("status") + search := c.Query("search") + + proxies, total, err := h.adminService.ListProxies(c.Request.Context(), page, pageSize, protocol, status, search) + if err != nil { + response.InternalError(c, "Failed to list proxies: "+err.Error()) + return + } + + response.Paginated(c, proxies, total, page, pageSize) +} + +// GetAll handles getting all active proxies without pagination +// GET /api/v1/admin/proxies/all +// Optional query param: with_count=true to include account count per proxy +func (h *ProxyHandler) GetAll(c *gin.Context) { + withCount := c.Query("with_count") == "true" + + if withCount { + proxies, err := h.adminService.GetAllProxiesWithAccountCount(c.Request.Context()) + if err != nil { + response.InternalError(c, "Failed to get proxies: "+err.Error()) + return + } + response.Success(c, proxies) + return + } + + proxies, err := h.adminService.GetAllProxies(c.Request.Context()) + if err != nil { + response.InternalError(c, "Failed to get proxies: "+err.Error()) + return + } + + response.Success(c, proxies) +} + +// GetByID handles getting a proxy by ID +// GET /api/v1/admin/proxies/:id +func (h *ProxyHandler) GetByID(c *gin.Context) { + proxyID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid proxy ID") + return + } + + proxy, err := h.adminService.GetProxy(c.Request.Context(), proxyID) + if err != nil { + response.NotFound(c, "Proxy not found") + return + } + + response.Success(c, proxy) +} + +// Create handles creating a new proxy +// POST /api/v1/admin/proxies +func (h *ProxyHandler) Create(c *gin.Context) { + var req CreateProxyRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + proxy, err := h.adminService.CreateProxy(c.Request.Context(), &service.CreateProxyInput{ + Name: req.Name, + Protocol: req.Protocol, + Host: req.Host, + Port: req.Port, + Username: req.Username, + Password: req.Password, + }) + if err != nil { + response.BadRequest(c, "Failed to create proxy: "+err.Error()) + return + } + + response.Success(c, proxy) +} + +// Update handles updating a proxy +// PUT /api/v1/admin/proxies/:id +func (h *ProxyHandler) Update(c *gin.Context) { + proxyID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid proxy ID") + return + } + + var req UpdateProxyRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + proxy, err := h.adminService.UpdateProxy(c.Request.Context(), proxyID, &service.UpdateProxyInput{ + Name: req.Name, + Protocol: req.Protocol, + Host: req.Host, + Port: req.Port, + Username: req.Username, + Password: req.Password, + Status: req.Status, + }) + if err != nil { + response.InternalError(c, "Failed to update proxy: "+err.Error()) + return + } + + response.Success(c, proxy) +} + +// Delete handles deleting a proxy +// DELETE /api/v1/admin/proxies/:id +func (h *ProxyHandler) Delete(c *gin.Context) { + proxyID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid proxy ID") + return + } + + err = h.adminService.DeleteProxy(c.Request.Context(), proxyID) + if err != nil { + response.InternalError(c, "Failed to delete proxy: "+err.Error()) + return + } + + response.Success(c, gin.H{"message": "Proxy deleted successfully"}) +} + +// Test handles testing proxy connectivity +// POST /api/v1/admin/proxies/:id/test +func (h *ProxyHandler) Test(c *gin.Context) { + proxyID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid proxy ID") + return + } + + result, err := h.adminService.TestProxy(c.Request.Context(), proxyID) + if err != nil { + response.InternalError(c, "Failed to test proxy: "+err.Error()) + return + } + + response.Success(c, result) +} + +// GetStats handles getting proxy statistics +// GET /api/v1/admin/proxies/:id/stats +func (h *ProxyHandler) GetStats(c *gin.Context) { + proxyID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid proxy ID") + return + } + + // Return mock data for now + _ = proxyID + response.Success(c, gin.H{ + "total_accounts": 0, + "active_accounts": 0, + "total_requests": 0, + "success_rate": 100.0, + "average_latency": 0, + }) +} + +// GetProxyAccounts handles getting accounts using a proxy +// GET /api/v1/admin/proxies/:id/accounts +func (h *ProxyHandler) GetProxyAccounts(c *gin.Context) { + proxyID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid proxy ID") + return + } + + page, pageSize := response.ParsePagination(c) + + accounts, total, err := h.adminService.GetProxyAccounts(c.Request.Context(), proxyID, page, pageSize) + if err != nil { + response.InternalError(c, "Failed to get proxy accounts: "+err.Error()) + return + } + + response.Paginated(c, accounts, total, page, pageSize) +} + + +// BatchCreateProxyItem represents a single proxy in batch create request +type BatchCreateProxyItem struct { + Protocol string `json:"protocol" binding:"required,oneof=http https socks5"` + Host string `json:"host" binding:"required"` + Port int `json:"port" binding:"required,min=1,max=65535"` + Username string `json:"username"` + Password string `json:"password"` +} + +// BatchCreateRequest represents batch create proxies request +type BatchCreateRequest struct { + Proxies []BatchCreateProxyItem `json:"proxies" binding:"required,min=1"` +} + +// BatchCreate handles batch creating proxies +// POST /api/v1/admin/proxies/batch +func (h *ProxyHandler) BatchCreate(c *gin.Context) { + var req BatchCreateRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + created := 0 + skipped := 0 + + for _, item := range req.Proxies { + // Check for duplicates (same host, port, username, password) + exists, err := h.adminService.CheckProxyExists(c.Request.Context(), item.Host, item.Port, item.Username, item.Password) + if err != nil { + response.InternalError(c, "Failed to check proxy existence: "+err.Error()) + return + } + + if exists { + skipped++ + continue + } + + // Create proxy with default name + _, err = h.adminService.CreateProxy(c.Request.Context(), &service.CreateProxyInput{ + Name: "default", + Protocol: item.Protocol, + Host: item.Host, + Port: item.Port, + Username: item.Username, + Password: item.Password, + }) + if err != nil { + // If creation fails due to duplicate, count as skipped + skipped++ + continue + } + + created++ + } + + response.Success(c, gin.H{ + "created": created, + "skipped": skipped, + }) +} diff --git a/backend/internal/handler/admin/redeem_handler.go b/backend/internal/handler/admin/redeem_handler.go new file mode 100644 index 00000000..63856dca --- /dev/null +++ b/backend/internal/handler/admin/redeem_handler.go @@ -0,0 +1,219 @@ +package admin + +import ( + "bytes" + "encoding/csv" + "fmt" + "strconv" + + "sub2api/internal/pkg/response" + "sub2api/internal/service" + + "github.com/gin-gonic/gin" +) + +// RedeemHandler handles admin redeem code management +type RedeemHandler struct { + adminService service.AdminService +} + +// NewRedeemHandler creates a new admin redeem handler +func NewRedeemHandler(adminService service.AdminService) *RedeemHandler { + return &RedeemHandler{ + adminService: adminService, + } +} + +// GenerateRedeemCodesRequest represents generate redeem codes request +type GenerateRedeemCodesRequest struct { + Count int `json:"count" binding:"required,min=1,max=100"` + Type string `json:"type" binding:"required,oneof=balance concurrency subscription"` + Value float64 `json:"value" binding:"min=0"` + GroupID *int64 `json:"group_id"` // 订阅类型必填 + ValidityDays int `json:"validity_days"` // 订阅类型使用,默认30天 +} + +// List handles listing all redeem codes with pagination +// GET /api/v1/admin/redeem-codes +func (h *RedeemHandler) List(c *gin.Context) { + page, pageSize := response.ParsePagination(c) + codeType := c.Query("type") + status := c.Query("status") + search := c.Query("search") + + codes, total, err := h.adminService.ListRedeemCodes(c.Request.Context(), page, pageSize, codeType, status, search) + if err != nil { + response.InternalError(c, "Failed to list redeem codes: "+err.Error()) + return + } + + response.Paginated(c, codes, total, page, pageSize) +} + +// GetByID handles getting a redeem code by ID +// GET /api/v1/admin/redeem-codes/:id +func (h *RedeemHandler) GetByID(c *gin.Context) { + codeID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid redeem code ID") + return + } + + code, err := h.adminService.GetRedeemCode(c.Request.Context(), codeID) + if err != nil { + response.NotFound(c, "Redeem code not found") + return + } + + response.Success(c, code) +} + +// Generate handles generating new redeem codes +// POST /api/v1/admin/redeem-codes/generate +func (h *RedeemHandler) Generate(c *gin.Context) { + var req GenerateRedeemCodesRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + codes, err := h.adminService.GenerateRedeemCodes(c.Request.Context(), &service.GenerateRedeemCodesInput{ + Count: req.Count, + Type: req.Type, + Value: req.Value, + GroupID: req.GroupID, + ValidityDays: req.ValidityDays, + }) + if err != nil { + response.InternalError(c, "Failed to generate redeem codes: "+err.Error()) + return + } + + response.Success(c, codes) +} + +// Delete handles deleting a redeem code +// DELETE /api/v1/admin/redeem-codes/:id +func (h *RedeemHandler) Delete(c *gin.Context) { + codeID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid redeem code ID") + return + } + + err = h.adminService.DeleteRedeemCode(c.Request.Context(), codeID) + if err != nil { + response.InternalError(c, "Failed to delete redeem code: "+err.Error()) + return + } + + response.Success(c, gin.H{"message": "Redeem code deleted successfully"}) +} + +// BatchDelete handles batch deleting redeem codes +// POST /api/v1/admin/redeem-codes/batch-delete +func (h *RedeemHandler) BatchDelete(c *gin.Context) { + var req struct { + IDs []int64 `json:"ids" binding:"required,min=1"` + } + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + deleted, err := h.adminService.BatchDeleteRedeemCodes(c.Request.Context(), req.IDs) + if err != nil { + response.InternalError(c, "Failed to batch delete redeem codes: "+err.Error()) + return + } + + response.Success(c, gin.H{ + "deleted": deleted, + "message": "Redeem codes deleted successfully", + }) +} + +// Expire handles expiring a redeem code +// POST /api/v1/admin/redeem-codes/:id/expire +func (h *RedeemHandler) Expire(c *gin.Context) { + codeID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid redeem code ID") + return + } + + code, err := h.adminService.ExpireRedeemCode(c.Request.Context(), codeID) + if err != nil { + response.InternalError(c, "Failed to expire redeem code: "+err.Error()) + return + } + + response.Success(c, code) +} + +// GetStats handles getting redeem code statistics +// GET /api/v1/admin/redeem-codes/stats +func (h *RedeemHandler) GetStats(c *gin.Context) { + // Return mock data for now + response.Success(c, gin.H{ + "total_codes": 0, + "active_codes": 0, + "used_codes": 0, + "expired_codes": 0, + "total_value_distributed": 0.0, + "by_type": gin.H{ + "balance": 0, + "concurrency": 0, + "trial": 0, + }, + }) +} + +// Export handles exporting redeem codes to CSV +// GET /api/v1/admin/redeem-codes/export +func (h *RedeemHandler) Export(c *gin.Context) { + codeType := c.Query("type") + status := c.Query("status") + + // Get all codes without pagination (use large page size) + codes, _, err := h.adminService.ListRedeemCodes(c.Request.Context(), 1, 10000, codeType, status, "") + if err != nil { + response.InternalError(c, "Failed to export redeem codes: "+err.Error()) + return + } + + // Create CSV buffer + var buf bytes.Buffer + writer := csv.NewWriter(&buf) + + // Write header + writer.Write([]string{"id", "code", "type", "value", "status", "used_by", "used_at", "created_at"}) + + // Write data rows + for _, code := range codes { + usedBy := "" + if code.UsedBy != nil { + usedBy = fmt.Sprintf("%d", *code.UsedBy) + } + usedAt := "" + if code.UsedAt != nil { + usedAt = code.UsedAt.Format("2006-01-02 15:04:05") + } + writer.Write([]string{ + fmt.Sprintf("%d", code.ID), + code.Code, + code.Type, + fmt.Sprintf("%.2f", code.Value), + code.Status, + usedBy, + usedAt, + code.CreatedAt.Format("2006-01-02 15:04:05"), + }) + } + + writer.Flush() + + c.Header("Content-Type", "text/csv") + c.Header("Content-Disposition", "attachment; filename=redeem_codes.csv") + c.Data(200, "text/csv", buf.Bytes()) +} diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go new file mode 100644 index 00000000..5fe4dc7e --- /dev/null +++ b/backend/internal/handler/admin/setting_handler.go @@ -0,0 +1,258 @@ +package admin + +import ( + "sub2api/internal/model" + "sub2api/internal/pkg/response" + "sub2api/internal/service" + + "github.com/gin-gonic/gin" +) + +// SettingHandler 系统设置处理器 +type SettingHandler struct { + settingService *service.SettingService + emailService *service.EmailService +} + +// NewSettingHandler 创建系统设置处理器 +func NewSettingHandler(settingService *service.SettingService, emailService *service.EmailService) *SettingHandler { + return &SettingHandler{ + settingService: settingService, + emailService: emailService, + } +} + +// GetSettings 获取所有系统设置 +// GET /api/v1/admin/settings +func (h *SettingHandler) GetSettings(c *gin.Context) { + settings, err := h.settingService.GetAllSettings(c.Request.Context()) + if err != nil { + response.InternalError(c, "Failed to get settings: "+err.Error()) + return + } + + response.Success(c, settings) +} + +// UpdateSettingsRequest 更新设置请求 +type UpdateSettingsRequest struct { + // 注册设置 + RegistrationEnabled bool `json:"registration_enabled"` + EmailVerifyEnabled bool `json:"email_verify_enabled"` + + // 邮件服务设置 + SmtpHost string `json:"smtp_host"` + SmtpPort int `json:"smtp_port"` + SmtpUsername string `json:"smtp_username"` + SmtpPassword string `json:"smtp_password"` + SmtpFrom string `json:"smtp_from_email"` + SmtpFromName string `json:"smtp_from_name"` + SmtpUseTLS bool `json:"smtp_use_tls"` + + // Cloudflare Turnstile 设置 + TurnstileEnabled bool `json:"turnstile_enabled"` + TurnstileSiteKey string `json:"turnstile_site_key"` + TurnstileSecretKey string `json:"turnstile_secret_key"` + + // OEM设置 + SiteName string `json:"site_name"` + SiteLogo string `json:"site_logo"` + SiteSubtitle string `json:"site_subtitle"` + ApiBaseUrl string `json:"api_base_url"` + ContactInfo string `json:"contact_info"` + + // 默认配置 + DefaultConcurrency int `json:"default_concurrency"` + DefaultBalance float64 `json:"default_balance"` +} + +// UpdateSettings 更新系统设置 +// PUT /api/v1/admin/settings +func (h *SettingHandler) UpdateSettings(c *gin.Context) { + var req UpdateSettingsRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + // 验证参数 + if req.DefaultConcurrency < 1 { + req.DefaultConcurrency = 1 + } + if req.DefaultBalance < 0 { + req.DefaultBalance = 0 + } + if req.SmtpPort <= 0 { + req.SmtpPort = 587 + } + + settings := &model.SystemSettings{ + RegistrationEnabled: req.RegistrationEnabled, + EmailVerifyEnabled: req.EmailVerifyEnabled, + SmtpHost: req.SmtpHost, + SmtpPort: req.SmtpPort, + SmtpUsername: req.SmtpUsername, + SmtpPassword: req.SmtpPassword, + SmtpFrom: req.SmtpFrom, + SmtpFromName: req.SmtpFromName, + SmtpUseTLS: req.SmtpUseTLS, + TurnstileEnabled: req.TurnstileEnabled, + TurnstileSiteKey: req.TurnstileSiteKey, + TurnstileSecretKey: req.TurnstileSecretKey, + SiteName: req.SiteName, + SiteLogo: req.SiteLogo, + SiteSubtitle: req.SiteSubtitle, + ApiBaseUrl: req.ApiBaseUrl, + ContactInfo: req.ContactInfo, + DefaultConcurrency: req.DefaultConcurrency, + DefaultBalance: req.DefaultBalance, + } + + if err := h.settingService.UpdateSettings(c.Request.Context(), settings); err != nil { + response.InternalError(c, "Failed to update settings: "+err.Error()) + return + } + + // 重新获取设置返回 + updatedSettings, err := h.settingService.GetAllSettings(c.Request.Context()) + if err != nil { + response.InternalError(c, "Failed to get updated settings: "+err.Error()) + return + } + + response.Success(c, updatedSettings) +} + +// TestSmtpRequest 测试SMTP连接请求 +type TestSmtpRequest struct { + SmtpHost string `json:"smtp_host" binding:"required"` + SmtpPort int `json:"smtp_port"` + SmtpUsername string `json:"smtp_username"` + SmtpPassword string `json:"smtp_password"` + SmtpUseTLS bool `json:"smtp_use_tls"` +} + +// TestSmtpConnection 测试SMTP连接 +// POST /api/v1/admin/settings/test-smtp +func (h *SettingHandler) TestSmtpConnection(c *gin.Context) { + var req TestSmtpRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + if req.SmtpPort <= 0 { + req.SmtpPort = 587 + } + + // 如果未提供密码,从数据库获取已保存的密码 + password := req.SmtpPassword + if password == "" { + savedConfig, err := h.emailService.GetSmtpConfig(c.Request.Context()) + if err == nil && savedConfig != nil { + password = savedConfig.Password + } + } + + config := &service.SmtpConfig{ + Host: req.SmtpHost, + Port: req.SmtpPort, + Username: req.SmtpUsername, + Password: password, + UseTLS: req.SmtpUseTLS, + } + + err := h.emailService.TestSmtpConnectionWithConfig(config) + if err != nil { + response.BadRequest(c, "SMTP connection test failed: "+err.Error()) + return + } + + response.Success(c, gin.H{"message": "SMTP connection successful"}) +} + +// SendTestEmailRequest 发送测试邮件请求 +type SendTestEmailRequest struct { + Email string `json:"email" binding:"required,email"` + SmtpHost string `json:"smtp_host" binding:"required"` + SmtpPort int `json:"smtp_port"` + SmtpUsername string `json:"smtp_username"` + SmtpPassword string `json:"smtp_password"` + SmtpFrom string `json:"smtp_from_email"` + SmtpFromName string `json:"smtp_from_name"` + SmtpUseTLS bool `json:"smtp_use_tls"` +} + +// SendTestEmail 发送测试邮件 +// POST /api/v1/admin/settings/send-test-email +func (h *SettingHandler) SendTestEmail(c *gin.Context) { + var req SendTestEmailRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + if req.SmtpPort <= 0 { + req.SmtpPort = 587 + } + + // 如果未提供密码,从数据库获取已保存的密码 + password := req.SmtpPassword + if password == "" { + savedConfig, err := h.emailService.GetSmtpConfig(c.Request.Context()) + if err == nil && savedConfig != nil { + password = savedConfig.Password + } + } + + config := &service.SmtpConfig{ + Host: req.SmtpHost, + Port: req.SmtpPort, + Username: req.SmtpUsername, + Password: password, + From: req.SmtpFrom, + FromName: req.SmtpFromName, + UseTLS: req.SmtpUseTLS, + } + + siteName := h.settingService.GetSiteName(c.Request.Context()) + subject := "[" + siteName + "] Test Email" + body := ` + + + + + + + +
+
+

` + siteName + `

+
+
+
+

Email Configuration Successful!

+

This is a test email to verify your SMTP settings are working correctly.

+
+ +
+ + +` + + if err := h.emailService.SendEmailWithConfig(config, req.Email, subject, body); err != nil { + response.BadRequest(c, "Failed to send test email: "+err.Error()) + return + } + + response.Success(c, gin.H{"message": "Test email sent successfully"}) +} diff --git a/backend/internal/handler/admin/subscription_handler.go b/backend/internal/handler/admin/subscription_handler.go new file mode 100644 index 00000000..0d83e848 --- /dev/null +++ b/backend/internal/handler/admin/subscription_handler.go @@ -0,0 +1,266 @@ +package admin + +import ( + "strconv" + + "sub2api/internal/model" + "sub2api/internal/pkg/response" + "sub2api/internal/repository" + "sub2api/internal/service" + + "github.com/gin-gonic/gin" +) + +// toResponsePagination converts repository.PaginationResult to response.PaginationResult +func toResponsePagination(p *repository.PaginationResult) *response.PaginationResult { + if p == nil { + return nil + } + return &response.PaginationResult{ + Total: p.Total, + Page: p.Page, + PageSize: p.PageSize, + Pages: p.Pages, + } +} + +// SubscriptionHandler handles admin subscription management +type SubscriptionHandler struct { + subscriptionService *service.SubscriptionService +} + +// NewSubscriptionHandler creates a new admin subscription handler +func NewSubscriptionHandler(subscriptionService *service.SubscriptionService) *SubscriptionHandler { + return &SubscriptionHandler{ + subscriptionService: subscriptionService, + } +} + +// AssignSubscriptionRequest represents assign subscription request +type AssignSubscriptionRequest struct { + UserID int64 `json:"user_id" binding:"required"` + GroupID int64 `json:"group_id" binding:"required"` + ValidityDays int `json:"validity_days"` + Notes string `json:"notes"` +} + +// BulkAssignSubscriptionRequest represents bulk assign subscription request +type BulkAssignSubscriptionRequest struct { + UserIDs []int64 `json:"user_ids" binding:"required,min=1"` + GroupID int64 `json:"group_id" binding:"required"` + ValidityDays int `json:"validity_days"` + Notes string `json:"notes"` +} + +// ExtendSubscriptionRequest represents extend subscription request +type ExtendSubscriptionRequest struct { + Days int `json:"days" binding:"required,min=1"` +} + +// List handles listing all subscriptions with pagination and filters +// GET /api/v1/admin/subscriptions +func (h *SubscriptionHandler) List(c *gin.Context) { + page, pageSize := response.ParsePagination(c) + + // Parse optional filters + var userID, groupID *int64 + if userIDStr := c.Query("user_id"); userIDStr != "" { + if id, err := strconv.ParseInt(userIDStr, 10, 64); err == nil { + userID = &id + } + } + if groupIDStr := c.Query("group_id"); groupIDStr != "" { + if id, err := strconv.ParseInt(groupIDStr, 10, 64); err == nil { + groupID = &id + } + } + status := c.Query("status") + + subscriptions, pagination, err := h.subscriptionService.List(c.Request.Context(), page, pageSize, userID, groupID, status) + if err != nil { + response.InternalError(c, "Failed to list subscriptions: "+err.Error()) + return + } + + response.PaginatedWithResult(c, subscriptions, toResponsePagination(pagination)) +} + +// GetByID handles getting a subscription by ID +// GET /api/v1/admin/subscriptions/:id +func (h *SubscriptionHandler) GetByID(c *gin.Context) { + subscriptionID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid subscription ID") + return + } + + subscription, err := h.subscriptionService.GetByID(c.Request.Context(), subscriptionID) + if err != nil { + response.NotFound(c, "Subscription not found") + return + } + + response.Success(c, subscription) +} + +// GetProgress handles getting subscription usage progress +// GET /api/v1/admin/subscriptions/:id/progress +func (h *SubscriptionHandler) GetProgress(c *gin.Context) { + subscriptionID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid subscription ID") + return + } + + progress, err := h.subscriptionService.GetSubscriptionProgress(c.Request.Context(), subscriptionID) + if err != nil { + response.NotFound(c, "Subscription not found") + return + } + + response.Success(c, progress) +} + +// Assign handles assigning a subscription to a user +// POST /api/v1/admin/subscriptions/assign +func (h *SubscriptionHandler) Assign(c *gin.Context) { + var req AssignSubscriptionRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + // Get admin user ID from context + adminID := getAdminIDFromContext(c) + + subscription, err := h.subscriptionService.AssignSubscription(c.Request.Context(), &service.AssignSubscriptionInput{ + UserID: req.UserID, + GroupID: req.GroupID, + ValidityDays: req.ValidityDays, + AssignedBy: adminID, + Notes: req.Notes, + }) + if err != nil { + response.BadRequest(c, "Failed to assign subscription: "+err.Error()) + return + } + + response.Success(c, subscription) +} + +// BulkAssign handles bulk assigning subscriptions to multiple users +// POST /api/v1/admin/subscriptions/bulk-assign +func (h *SubscriptionHandler) BulkAssign(c *gin.Context) { + var req BulkAssignSubscriptionRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + // Get admin user ID from context + adminID := getAdminIDFromContext(c) + + result, err := h.subscriptionService.BulkAssignSubscription(c.Request.Context(), &service.BulkAssignSubscriptionInput{ + UserIDs: req.UserIDs, + GroupID: req.GroupID, + ValidityDays: req.ValidityDays, + AssignedBy: adminID, + Notes: req.Notes, + }) + if err != nil { + response.InternalError(c, "Failed to bulk assign subscriptions: "+err.Error()) + return + } + + response.Success(c, result) +} + +// Extend handles extending a subscription +// POST /api/v1/admin/subscriptions/:id/extend +func (h *SubscriptionHandler) Extend(c *gin.Context) { + subscriptionID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid subscription ID") + return + } + + var req ExtendSubscriptionRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + subscription, err := h.subscriptionService.ExtendSubscription(c.Request.Context(), subscriptionID, req.Days) + if err != nil { + response.InternalError(c, "Failed to extend subscription: "+err.Error()) + return + } + + response.Success(c, subscription) +} + +// Revoke handles revoking a subscription +// DELETE /api/v1/admin/subscriptions/:id +func (h *SubscriptionHandler) Revoke(c *gin.Context) { + subscriptionID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid subscription ID") + return + } + + err = h.subscriptionService.RevokeSubscription(c.Request.Context(), subscriptionID) + if err != nil { + response.InternalError(c, "Failed to revoke subscription: "+err.Error()) + return + } + + response.Success(c, gin.H{"message": "Subscription revoked successfully"}) +} + +// ListByGroup handles listing subscriptions for a specific group +// GET /api/v1/admin/groups/:id/subscriptions +func (h *SubscriptionHandler) ListByGroup(c *gin.Context) { + groupID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid group ID") + return + } + + page, pageSize := response.ParsePagination(c) + + subscriptions, pagination, err := h.subscriptionService.ListGroupSubscriptions(c.Request.Context(), groupID, page, pageSize) + if err != nil { + response.InternalError(c, "Failed to list group subscriptions: "+err.Error()) + return + } + + response.PaginatedWithResult(c, subscriptions, toResponsePagination(pagination)) +} + +// ListByUser handles listing subscriptions for a specific user +// GET /api/v1/admin/users/:id/subscriptions +func (h *SubscriptionHandler) ListByUser(c *gin.Context) { + userID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid user ID") + return + } + + subscriptions, err := h.subscriptionService.ListUserSubscriptions(c.Request.Context(), userID) + if err != nil { + response.InternalError(c, "Failed to list user subscriptions: "+err.Error()) + return + } + + response.Success(c, subscriptions) +} + +// Helper function to get admin ID from context +func getAdminIDFromContext(c *gin.Context) int64 { + if user, exists := c.Get("user"); exists { + if u, ok := user.(*model.User); ok && u != nil { + return u.ID + } + } + return 0 +} diff --git a/backend/internal/handler/admin/system_handler.go b/backend/internal/handler/admin/system_handler.go new file mode 100644 index 00000000..ae6613a3 --- /dev/null +++ b/backend/internal/handler/admin/system_handler.go @@ -0,0 +1,82 @@ +package admin + +import ( + "net/http" + + "sub2api/internal/pkg/response" + "sub2api/internal/service" + + "github.com/gin-gonic/gin" + "github.com/redis/go-redis/v9" +) + +// SystemHandler handles system-related operations +type SystemHandler struct { + updateSvc *service.UpdateService +} + +// NewSystemHandler creates a new SystemHandler +func NewSystemHandler(rdb *redis.Client, version, buildType string) *SystemHandler { + return &SystemHandler{ + updateSvc: service.NewUpdateService(rdb, version, buildType), + } +} + +// GetVersion returns the current version +// GET /api/v1/admin/system/version +func (h *SystemHandler) GetVersion(c *gin.Context) { + info, _ := h.updateSvc.CheckUpdate(c.Request.Context(), false) + response.Success(c, gin.H{ + "version": info.CurrentVersion, + }) +} + +// CheckUpdates checks for available updates +// GET /api/v1/admin/system/check-updates +func (h *SystemHandler) CheckUpdates(c *gin.Context) { + force := c.Query("force") == "true" + info, err := h.updateSvc.CheckUpdate(c.Request.Context(), force) + if err != nil { + response.Error(c, http.StatusInternalServerError, err.Error()) + return + } + response.Success(c, info) +} + +// PerformUpdate downloads and applies the update +// POST /api/v1/admin/system/update +func (h *SystemHandler) PerformUpdate(c *gin.Context) { + if err := h.updateSvc.PerformUpdate(c.Request.Context()); err != nil { + response.Error(c, http.StatusInternalServerError, err.Error()) + return + } + response.Success(c, gin.H{ + "message": "Update completed. Please restart the service.", + "need_restart": true, + }) +} + +// Rollback restores the previous version +// POST /api/v1/admin/system/rollback +func (h *SystemHandler) Rollback(c *gin.Context) { + if err := h.updateSvc.Rollback(); err != nil { + response.Error(c, http.StatusInternalServerError, err.Error()) + return + } + response.Success(c, gin.H{ + "message": "Rollback completed. Please restart the service.", + "need_restart": true, + }) +} + +// RestartService restarts the systemd service +// POST /api/v1/admin/system/restart +func (h *SystemHandler) RestartService(c *gin.Context) { + if err := h.updateSvc.RestartService(); err != nil { + response.Error(c, http.StatusInternalServerError, err.Error()) + return + } + response.Success(c, gin.H{ + "message": "Service restart initiated", + }) +} diff --git a/backend/internal/handler/admin/usage_handler.go b/backend/internal/handler/admin/usage_handler.go new file mode 100644 index 00000000..a0154c12 --- /dev/null +++ b/backend/internal/handler/admin/usage_handler.go @@ -0,0 +1,262 @@ +package admin + +import ( + "strconv" + "time" + + "sub2api/internal/pkg/response" + "sub2api/internal/pkg/timezone" + "sub2api/internal/repository" + "sub2api/internal/service" + + "github.com/gin-gonic/gin" +) + +// UsageHandler handles admin usage-related requests +type UsageHandler struct { + usageRepo *repository.UsageLogRepository + apiKeyRepo *repository.ApiKeyRepository + usageService *service.UsageService + adminService service.AdminService +} + +// NewUsageHandler creates a new admin usage handler +func NewUsageHandler( + usageRepo *repository.UsageLogRepository, + apiKeyRepo *repository.ApiKeyRepository, + usageService *service.UsageService, + adminService service.AdminService, +) *UsageHandler { + return &UsageHandler{ + usageRepo: usageRepo, + apiKeyRepo: apiKeyRepo, + usageService: usageService, + adminService: adminService, + } +} + +// List handles listing all usage records with filters +// GET /api/v1/admin/usage +func (h *UsageHandler) List(c *gin.Context) { + page, pageSize := response.ParsePagination(c) + + // Parse filters + var userID, apiKeyID int64 + if userIDStr := c.Query("user_id"); userIDStr != "" { + id, err := strconv.ParseInt(userIDStr, 10, 64) + if err != nil { + response.BadRequest(c, "Invalid user_id") + return + } + userID = id + } + + if apiKeyIDStr := c.Query("api_key_id"); apiKeyIDStr != "" { + id, err := strconv.ParseInt(apiKeyIDStr, 10, 64) + if err != nil { + response.BadRequest(c, "Invalid api_key_id") + return + } + apiKeyID = id + } + + // Parse date range + var startTime, endTime *time.Time + if startDateStr := c.Query("start_date"); startDateStr != "" { + t, err := timezone.ParseInLocation("2006-01-02", startDateStr) + if err != nil { + response.BadRequest(c, "Invalid start_date format, use YYYY-MM-DD") + return + } + startTime = &t + } + + if endDateStr := c.Query("end_date"); endDateStr != "" { + t, err := timezone.ParseInLocation("2006-01-02", endDateStr) + if err != nil { + response.BadRequest(c, "Invalid end_date format, use YYYY-MM-DD") + return + } + // Set end time to end of day + t = t.Add(24*time.Hour - time.Nanosecond) + endTime = &t + } + + params := repository.PaginationParams{Page: page, PageSize: pageSize} + filters := repository.UsageLogFilters{ + UserID: userID, + ApiKeyID: apiKeyID, + StartTime: startTime, + EndTime: endTime, + } + + records, result, err := h.usageRepo.ListWithFilters(c.Request.Context(), params, filters) + if err != nil { + response.InternalError(c, "Failed to list usage records: "+err.Error()) + return + } + + response.Paginated(c, records, result.Total, page, pageSize) +} + +// Stats handles getting usage statistics with filters +// GET /api/v1/admin/usage/stats +func (h *UsageHandler) Stats(c *gin.Context) { + // Parse filters + var userID, apiKeyID int64 + if userIDStr := c.Query("user_id"); userIDStr != "" { + id, err := strconv.ParseInt(userIDStr, 10, 64) + if err != nil { + response.BadRequest(c, "Invalid user_id") + return + } + userID = id + } + + if apiKeyIDStr := c.Query("api_key_id"); apiKeyIDStr != "" { + id, err := strconv.ParseInt(apiKeyIDStr, 10, 64) + if err != nil { + response.BadRequest(c, "Invalid api_key_id") + return + } + apiKeyID = id + } + + // Parse date range + now := timezone.Now() + var startTime, endTime time.Time + + startDateStr := c.Query("start_date") + endDateStr := c.Query("end_date") + + if startDateStr != "" && endDateStr != "" { + var err error + startTime, err = timezone.ParseInLocation("2006-01-02", startDateStr) + if err != nil { + response.BadRequest(c, "Invalid start_date format, use YYYY-MM-DD") + return + } + endTime, err = timezone.ParseInLocation("2006-01-02", endDateStr) + if err != nil { + response.BadRequest(c, "Invalid end_date format, use YYYY-MM-DD") + return + } + endTime = endTime.Add(24*time.Hour - time.Nanosecond) + } else { + period := c.DefaultQuery("period", "today") + switch period { + case "today": + startTime = timezone.StartOfDay(now) + case "week": + startTime = now.AddDate(0, 0, -7) + case "month": + startTime = now.AddDate(0, -1, 0) + default: + startTime = timezone.StartOfDay(now) + } + endTime = now + } + + if apiKeyID > 0 { + stats, err := h.usageService.GetStatsByApiKey(c.Request.Context(), apiKeyID, startTime, endTime) + if err != nil { + response.InternalError(c, "Failed to get usage statistics: "+err.Error()) + return + } + response.Success(c, stats) + return + } + + if userID > 0 { + stats, err := h.usageService.GetStatsByUser(c.Request.Context(), userID, startTime, endTime) + if err != nil { + response.InternalError(c, "Failed to get usage statistics: "+err.Error()) + return + } + response.Success(c, stats) + return + } + + // Get global stats + stats, err := h.usageRepo.GetGlobalStats(c.Request.Context(), startTime, endTime) + if err != nil { + response.InternalError(c, "Failed to get usage statistics: "+err.Error()) + return + } + + response.Success(c, stats) +} + +// SearchUsers handles searching users by email keyword +// GET /api/v1/admin/usage/search-users +func (h *UsageHandler) SearchUsers(c *gin.Context) { + keyword := c.Query("q") + if keyword == "" { + response.Success(c, []interface{}{}) + return + } + + // Limit to 30 results + users, _, err := h.adminService.ListUsers(c.Request.Context(), 1, 30, "", "", keyword) + if err != nil { + response.InternalError(c, "Failed to search users: "+err.Error()) + return + } + + // Return simplified user list (only id and email) + type SimpleUser struct { + ID int64 `json:"id"` + Email string `json:"email"` + } + + result := make([]SimpleUser, len(users)) + for i, u := range users { + result[i] = SimpleUser{ + ID: u.ID, + Email: u.Email, + } + } + + response.Success(c, result) +} + +// SearchApiKeys handles searching API keys by user +// GET /api/v1/admin/usage/search-api-keys +func (h *UsageHandler) SearchApiKeys(c *gin.Context) { + userIDStr := c.Query("user_id") + keyword := c.Query("q") + + var userID int64 + if userIDStr != "" { + id, err := strconv.ParseInt(userIDStr, 10, 64) + if err != nil { + response.BadRequest(c, "Invalid user_id") + return + } + userID = id + } + + keys, err := h.apiKeyRepo.SearchApiKeys(c.Request.Context(), userID, keyword, 30) + if err != nil { + response.InternalError(c, "Failed to search API keys: "+err.Error()) + return + } + + // Return simplified API key list (only id and name) + type SimpleApiKey struct { + ID int64 `json:"id"` + Name string `json:"name"` + UserID int64 `json:"user_id"` + } + + result := make([]SimpleApiKey, len(keys)) + for i, k := range keys { + result[i] = SimpleApiKey{ + ID: k.ID, + Name: k.Name, + UserID: k.UserID, + } + } + + response.Success(c, result) +} diff --git a/backend/internal/handler/admin/user_handler.go b/backend/internal/handler/admin/user_handler.go new file mode 100644 index 00000000..4d4d5d7b --- /dev/null +++ b/backend/internal/handler/admin/user_handler.go @@ -0,0 +1,221 @@ +package admin + +import ( + "strconv" + + "sub2api/internal/pkg/response" + "sub2api/internal/service" + + "github.com/gin-gonic/gin" +) + +// UserHandler handles admin user management +type UserHandler struct { + adminService service.AdminService +} + +// NewUserHandler creates a new admin user handler +func NewUserHandler(adminService service.AdminService) *UserHandler { + return &UserHandler{ + adminService: adminService, + } +} + +// CreateUserRequest represents admin create user request +type CreateUserRequest struct { + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required,min=6"` + Balance float64 `json:"balance"` + Concurrency int `json:"concurrency"` + AllowedGroups []int64 `json:"allowed_groups"` +} + +// UpdateUserRequest represents admin update user request +// 使用指针类型来区分"未提供"和"设置为0" +type UpdateUserRequest struct { + Email string `json:"email" binding:"omitempty,email"` + Password string `json:"password" binding:"omitempty,min=6"` + Balance *float64 `json:"balance"` + Concurrency *int `json:"concurrency"` + Status string `json:"status" binding:"omitempty,oneof=active disabled"` + AllowedGroups *[]int64 `json:"allowed_groups"` +} + +// UpdateBalanceRequest represents balance update request +type UpdateBalanceRequest struct { + Balance float64 `json:"balance" binding:"required"` + Operation string `json:"operation" binding:"required,oneof=set add subtract"` +} + +// List handles listing all users with pagination +// GET /api/v1/admin/users +func (h *UserHandler) List(c *gin.Context) { + page, pageSize := response.ParsePagination(c) + status := c.Query("status") + role := c.Query("role") + search := c.Query("search") + + users, total, err := h.adminService.ListUsers(c.Request.Context(), page, pageSize, status, role, search) + if err != nil { + response.InternalError(c, "Failed to list users: "+err.Error()) + return + } + + response.Paginated(c, users, total, page, pageSize) +} + +// GetByID handles getting a user by ID +// GET /api/v1/admin/users/:id +func (h *UserHandler) GetByID(c *gin.Context) { + userID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid user ID") + return + } + + user, err := h.adminService.GetUser(c.Request.Context(), userID) + if err != nil { + response.NotFound(c, "User not found") + return + } + + response.Success(c, user) +} + +// Create handles creating a new user +// POST /api/v1/admin/users +func (h *UserHandler) Create(c *gin.Context) { + var req CreateUserRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + user, err := h.adminService.CreateUser(c.Request.Context(), &service.CreateUserInput{ + Email: req.Email, + Password: req.Password, + Balance: req.Balance, + Concurrency: req.Concurrency, + AllowedGroups: req.AllowedGroups, + }) + if err != nil { + response.BadRequest(c, "Failed to create user: "+err.Error()) + return + } + + response.Success(c, user) +} + +// Update handles updating a user +// PUT /api/v1/admin/users/:id +func (h *UserHandler) Update(c *gin.Context) { + userID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid user ID") + return + } + + var req UpdateUserRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + // 使用指针类型直接传递,nil 表示未提供该字段 + user, err := h.adminService.UpdateUser(c.Request.Context(), userID, &service.UpdateUserInput{ + Email: req.Email, + Password: req.Password, + Balance: req.Balance, + Concurrency: req.Concurrency, + Status: req.Status, + AllowedGroups: req.AllowedGroups, + }) + if err != nil { + response.InternalError(c, "Failed to update user: "+err.Error()) + return + } + + response.Success(c, user) +} + +// Delete handles deleting a user +// DELETE /api/v1/admin/users/:id +func (h *UserHandler) Delete(c *gin.Context) { + userID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid user ID") + return + } + + err = h.adminService.DeleteUser(c.Request.Context(), userID) + if err != nil { + response.InternalError(c, "Failed to delete user: "+err.Error()) + return + } + + response.Success(c, gin.H{"message": "User deleted successfully"}) +} + +// UpdateBalance handles updating user balance +// POST /api/v1/admin/users/:id/balance +func (h *UserHandler) UpdateBalance(c *gin.Context) { + userID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid user ID") + return + } + + var req UpdateBalanceRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + user, err := h.adminService.UpdateUserBalance(c.Request.Context(), userID, req.Balance, req.Operation) + if err != nil { + response.InternalError(c, "Failed to update balance: "+err.Error()) + return + } + + response.Success(c, user) +} + +// GetUserAPIKeys handles getting user's API keys +// GET /api/v1/admin/users/:id/api-keys +func (h *UserHandler) GetUserAPIKeys(c *gin.Context) { + userID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid user ID") + return + } + + page, pageSize := response.ParsePagination(c) + + keys, total, err := h.adminService.GetUserAPIKeys(c.Request.Context(), userID, page, pageSize) + if err != nil { + response.InternalError(c, "Failed to get user API keys: "+err.Error()) + return + } + + response.Paginated(c, keys, total, page, pageSize) +} + +// GetUserUsage handles getting user's usage statistics +// GET /api/v1/admin/users/:id/usage +func (h *UserHandler) GetUserUsage(c *gin.Context) { + userID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid user ID") + return + } + + period := c.DefaultQuery("period", "month") + + stats, err := h.adminService.GetUserUsageStats(c.Request.Context(), userID, period) + if err != nil { + response.InternalError(c, "Failed to get user usage: "+err.Error()) + return + } + + response.Success(c, stats) +} diff --git a/backend/internal/handler/api_key_handler.go b/backend/internal/handler/api_key_handler.go new file mode 100644 index 00000000..3e4a5733 --- /dev/null +++ b/backend/internal/handler/api_key_handler.go @@ -0,0 +1,235 @@ +package handler + +import ( + "strconv" + + "sub2api/internal/model" + "sub2api/internal/pkg/response" + "sub2api/internal/repository" + "sub2api/internal/service" + + "github.com/gin-gonic/gin" +) + +// APIKeyHandler handles API key-related requests +type APIKeyHandler struct { + apiKeyService *service.ApiKeyService +} + +// NewAPIKeyHandler creates a new APIKeyHandler +func NewAPIKeyHandler(apiKeyService *service.ApiKeyService) *APIKeyHandler { + return &APIKeyHandler{ + apiKeyService: apiKeyService, + } +} + +// CreateAPIKeyRequest represents the create API key request payload +type CreateAPIKeyRequest struct { + Name string `json:"name" binding:"required"` + GroupID *int64 `json:"group_id"` // nullable + CustomKey *string `json:"custom_key"` // 可选的自定义key +} + +// UpdateAPIKeyRequest represents the update API key request payload +type UpdateAPIKeyRequest struct { + Name string `json:"name"` + GroupID *int64 `json:"group_id"` + Status string `json:"status" binding:"omitempty,oneof=active inactive"` +} + +// List handles listing user's API keys with pagination +// GET /api/v1/api-keys +func (h *APIKeyHandler) List(c *gin.Context) { + userValue, exists := c.Get("user") + if !exists { + response.Unauthorized(c, "User not authenticated") + return + } + + user, ok := userValue.(*model.User) + if !ok { + response.InternalError(c, "Invalid user context") + return + } + + page, pageSize := response.ParsePagination(c) + params := repository.PaginationParams{Page: page, PageSize: pageSize} + + keys, result, err := h.apiKeyService.List(c.Request.Context(), user.ID, params) + if err != nil { + response.InternalError(c, "Failed to list API keys: "+err.Error()) + return + } + + response.Paginated(c, keys, result.Total, page, pageSize) +} + +// GetByID handles getting a single API key +// GET /api/v1/api-keys/:id +func (h *APIKeyHandler) GetByID(c *gin.Context) { + userValue, exists := c.Get("user") + if !exists { + response.Unauthorized(c, "User not authenticated") + return + } + + user, ok := userValue.(*model.User) + if !ok { + response.InternalError(c, "Invalid user context") + return + } + + keyID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid key ID") + return + } + + key, err := h.apiKeyService.GetByID(c.Request.Context(), keyID) + if err != nil { + response.NotFound(c, "API key not found") + return + } + + // 验证所有权 + if key.UserID != user.ID { + response.Forbidden(c, "Not authorized to access this key") + return + } + + response.Success(c, key) +} + +// Create handles creating a new API key +// POST /api/v1/api-keys +func (h *APIKeyHandler) Create(c *gin.Context) { + userValue, exists := c.Get("user") + if !exists { + response.Unauthorized(c, "User not authenticated") + return + } + + user, ok := userValue.(*model.User) + if !ok { + response.InternalError(c, "Invalid user context") + return + } + + var req CreateAPIKeyRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + svcReq := service.CreateApiKeyRequest{ + Name: req.Name, + GroupID: req.GroupID, + CustomKey: req.CustomKey, + } + key, err := h.apiKeyService.Create(c.Request.Context(), user.ID, svcReq) + if err != nil { + response.InternalError(c, "Failed to create API key: "+err.Error()) + return + } + + response.Success(c, key) +} + +// Update handles updating an API key +// PUT /api/v1/api-keys/:id +func (h *APIKeyHandler) Update(c *gin.Context) { + userValue, exists := c.Get("user") + if !exists { + response.Unauthorized(c, "User not authenticated") + return + } + + user, ok := userValue.(*model.User) + if !ok { + response.InternalError(c, "Invalid user context") + return + } + + keyID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid key ID") + return + } + + var req UpdateAPIKeyRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + svcReq := service.UpdateApiKeyRequest{} + if req.Name != "" { + svcReq.Name = &req.Name + } + svcReq.GroupID = req.GroupID + if req.Status != "" { + svcReq.Status = &req.Status + } + + key, err := h.apiKeyService.Update(c.Request.Context(), keyID, user.ID, svcReq) + if err != nil { + response.InternalError(c, "Failed to update API key: "+err.Error()) + return + } + + response.Success(c, key) +} + +// Delete handles deleting an API key +// DELETE /api/v1/api-keys/:id +func (h *APIKeyHandler) Delete(c *gin.Context) { + userValue, exists := c.Get("user") + if !exists { + response.Unauthorized(c, "User not authenticated") + return + } + + user, ok := userValue.(*model.User) + if !ok { + response.InternalError(c, "Invalid user context") + return + } + + keyID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid key ID") + return + } + + err = h.apiKeyService.Delete(c.Request.Context(), keyID, user.ID) + if err != nil { + response.InternalError(c, "Failed to delete API key: "+err.Error()) + return + } + + response.Success(c, gin.H{"message": "API key deleted successfully"}) +} + +// GetAvailableGroups 获取用户可以绑定的分组列表 +// GET /api/v1/groups/available +func (h *APIKeyHandler) GetAvailableGroups(c *gin.Context) { + userValue, exists := c.Get("user") + if !exists { + response.Unauthorized(c, "User not authenticated") + return + } + + user, ok := userValue.(*model.User) + if !ok { + response.InternalError(c, "Invalid user context") + return + } + + groups, err := h.apiKeyService.GetAvailableGroups(c.Request.Context(), user.ID) + if err != nil { + response.InternalError(c, "Failed to get available groups: "+err.Error()) + return + } + + response.Success(c, groups) +} diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go new file mode 100644 index 00000000..fe19040d --- /dev/null +++ b/backend/internal/handler/auth_handler.go @@ -0,0 +1,158 @@ +package handler + +import ( + "sub2api/internal/model" + "sub2api/internal/pkg/response" + "sub2api/internal/service" + + "github.com/gin-gonic/gin" +) + +// AuthHandler handles authentication-related requests +type AuthHandler struct { + authService *service.AuthService +} + +// NewAuthHandler creates a new AuthHandler +func NewAuthHandler(authService *service.AuthService) *AuthHandler { + return &AuthHandler{ + authService: authService, + } +} + +// RegisterRequest represents the registration request payload +type RegisterRequest struct { + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required,min=6"` + VerifyCode string `json:"verify_code"` + TurnstileToken string `json:"turnstile_token"` +} + +// SendVerifyCodeRequest 发送验证码请求 +type SendVerifyCodeRequest struct { + Email string `json:"email" binding:"required,email"` + TurnstileToken string `json:"turnstile_token"` +} + +// SendVerifyCodeResponse 发送验证码响应 +type SendVerifyCodeResponse struct { + Message string `json:"message"` + Countdown int `json:"countdown"` // 倒计时秒数 +} + +// LoginRequest represents the login request payload +type LoginRequest struct { + Email string `json:"email" binding:"required,email"` + Password string `json:"password" binding:"required"` + TurnstileToken string `json:"turnstile_token"` +} + +// AuthResponse 认证响应格式(匹配前端期望) +type AuthResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + User *model.User `json:"user"` +} + +// Register handles user registration +// POST /api/v1/auth/register +func (h *AuthHandler) Register(c *gin.Context) { + var req RegisterRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + // Turnstile 验证(当提供了邮箱验证码时跳过,因为发送验证码时已验证过) + if req.VerifyCode == "" { + if err := h.authService.VerifyTurnstile(c.Request.Context(), req.TurnstileToken, c.ClientIP()); err != nil { + response.BadRequest(c, "Turnstile verification failed: "+err.Error()) + return + } + } + + token, user, err := h.authService.RegisterWithVerification(c.Request.Context(), req.Email, req.Password, req.VerifyCode) + if err != nil { + response.BadRequest(c, "Registration failed: "+err.Error()) + return + } + + response.Success(c, AuthResponse{ + AccessToken: token, + TokenType: "Bearer", + User: user, + }) +} + +// SendVerifyCode 发送邮箱验证码 +// POST /api/v1/auth/send-verify-code +func (h *AuthHandler) SendVerifyCode(c *gin.Context) { + var req SendVerifyCodeRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + // Turnstile 验证 + if err := h.authService.VerifyTurnstile(c.Request.Context(), req.TurnstileToken, c.ClientIP()); err != nil { + response.BadRequest(c, "Turnstile verification failed: "+err.Error()) + return + } + + result, err := h.authService.SendVerifyCodeAsync(c.Request.Context(), req.Email) + if err != nil { + response.BadRequest(c, "Failed to send verification code: "+err.Error()) + return + } + + response.Success(c, SendVerifyCodeResponse{ + Message: "Verification code sent successfully", + Countdown: result.Countdown, + }) +} + +// Login handles user login +// POST /api/v1/auth/login +func (h *AuthHandler) Login(c *gin.Context) { + var req LoginRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + // Turnstile 验证 + if err := h.authService.VerifyTurnstile(c.Request.Context(), req.TurnstileToken, c.ClientIP()); err != nil { + response.BadRequest(c, "Turnstile verification failed: "+err.Error()) + return + } + + token, user, err := h.authService.Login(c.Request.Context(), req.Email, req.Password) + if err != nil { + response.Unauthorized(c, "Login failed: "+err.Error()) + return + } + + response.Success(c, AuthResponse{ + AccessToken: token, + TokenType: "Bearer", + User: user, + }) +} + +// GetCurrentUser handles getting current authenticated user +// GET /api/v1/auth/me +func (h *AuthHandler) GetCurrentUser(c *gin.Context) { + userValue, exists := c.Get("user") + if !exists { + response.Unauthorized(c, "User not authenticated") + return + } + + user, ok := userValue.(*model.User) + if !ok { + response.InternalError(c, "Invalid user context") + return + } + + response.Success(c, user) +} diff --git a/backend/internal/handler/gateway_handler.go b/backend/internal/handler/gateway_handler.go new file mode 100644 index 00000000..e620735d --- /dev/null +++ b/backend/internal/handler/gateway_handler.go @@ -0,0 +1,445 @@ +package handler + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "time" + + "sub2api/internal/middleware" + "sub2api/internal/model" + "sub2api/internal/service" + + "github.com/gin-gonic/gin" +) + +const ( + // Maximum wait time for concurrency slot + maxConcurrencyWait = 60 * time.Second + // Ping interval during wait + pingInterval = 5 * time.Second +) + +// GatewayHandler handles API gateway requests +type GatewayHandler struct { + gatewayService *service.GatewayService + userService *service.UserService + concurrencyService *service.ConcurrencyService + billingCacheService *service.BillingCacheService +} + +// NewGatewayHandler creates a new GatewayHandler +func NewGatewayHandler(gatewayService *service.GatewayService, userService *service.UserService, concurrencyService *service.ConcurrencyService, billingCacheService *service.BillingCacheService) *GatewayHandler { + return &GatewayHandler{ + gatewayService: gatewayService, + userService: userService, + concurrencyService: concurrencyService, + billingCacheService: billingCacheService, + } +} + +// Messages handles Claude API compatible messages endpoint +// POST /v1/messages +func (h *GatewayHandler) Messages(c *gin.Context) { + // 从context获取apiKey和user(ApiKeyAuth中间件已设置) + apiKey, ok := middleware.GetApiKeyFromContext(c) + if !ok { + h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key") + return + } + + user, ok := middleware.GetUserFromContext(c) + if !ok { + h.errorResponse(c, http.StatusInternalServerError, "api_error", "User context not found") + return + } + + // 读取请求体 + body, err := io.ReadAll(c.Request.Body) + if err != nil { + h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to read request body") + return + } + + if len(body) == 0 { + h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Request body is empty") + return + } + + // 解析请求获取模型名和stream + var req struct { + Model string `json:"model"` + Stream bool `json:"stream"` + } + if err := json.Unmarshal(body, &req); err != nil { + h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Failed to parse request body") + return + } + + // Track if we've started streaming (for error handling) + streamStarted := false + + // 获取订阅信息(可能为nil)- 提前获取用于后续检查 + subscription, _ := middleware.GetSubscriptionFromContext(c) + + // 0. 检查wait队列是否已满 + maxWait := service.CalculateMaxWait(user.Concurrency) + canWait, err := h.concurrencyService.IncrementWaitCount(c.Request.Context(), user.ID, maxWait) + if err != nil { + log.Printf("Increment wait count failed: %v", err) + // On error, allow request to proceed + } else if !canWait { + h.errorResponse(c, http.StatusTooManyRequests, "rate_limit_error", "Too many pending requests, please retry later") + return + } + // 确保在函数退出时减少wait计数 + defer h.concurrencyService.DecrementWaitCount(c.Request.Context(), user.ID) + + // 1. 首先获取用户并发槽位 + userReleaseFunc, err := h.acquireUserSlotWithWait(c, user, req.Stream, &streamStarted) + if err != nil { + log.Printf("User concurrency acquire failed: %v", err) + h.handleConcurrencyError(c, err, "user", streamStarted) + return + } + if userReleaseFunc != nil { + defer userReleaseFunc() + } + + // 2. 【新增】Wait后二次检查余额/订阅 + if err := h.billingCacheService.CheckBillingEligibility(c.Request.Context(), user, apiKey, apiKey.Group, subscription); err != nil { + log.Printf("Billing eligibility check failed after wait: %v", err) + h.handleStreamingAwareError(c, http.StatusForbidden, "billing_error", err.Error(), streamStarted) + return + } + + // 计算粘性会话hash + sessionHash := h.gatewayService.GenerateSessionHash(body) + + // 选择支持该模型的账号 + account, err := h.gatewayService.SelectAccountForModel(c.Request.Context(), apiKey.GroupID, sessionHash, req.Model) + if err != nil { + h.handleStreamingAwareError(c, http.StatusServiceUnavailable, "api_error", "No available accounts: "+err.Error(), streamStarted) + return + } + + // 3. 获取账号并发槽位 + accountReleaseFunc, err := h.acquireAccountSlotWithWait(c, account, req.Stream, &streamStarted) + if err != nil { + log.Printf("Account concurrency acquire failed: %v", err) + h.handleConcurrencyError(c, err, "account", streamStarted) + return + } + if accountReleaseFunc != nil { + defer accountReleaseFunc() + } + + // 转发请求 + result, err := h.gatewayService.Forward(c.Request.Context(), c, account, body) + if err != nil { + // 错误响应已在Forward中处理,这里只记录日志 + log.Printf("Forward request failed: %v", err) + return + } + + // 异步记录使用量(subscription已在函数开头获取) + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{ + Result: result, + ApiKey: apiKey, + User: user, + Account: account, + Subscription: subscription, + }); err != nil { + log.Printf("Record usage failed: %v", err) + } + }() +} + +// acquireUserSlotWithWait acquires a user concurrency slot, waiting if necessary +// For streaming requests, sends ping events during the wait +// streamStarted is updated if streaming response has begun +func (h *GatewayHandler) acquireUserSlotWithWait(c *gin.Context, user *model.User, isStream bool, streamStarted *bool) (func(), error) { + ctx := c.Request.Context() + + // Try to acquire immediately + result, err := h.concurrencyService.AcquireUserSlot(ctx, user.ID, user.Concurrency) + if err != nil { + return nil, err + } + + if result.Acquired { + return result.ReleaseFunc, nil + } + + // Need to wait - handle streaming ping if needed + return h.waitForSlotWithPing(c, "user", user.ID, user.Concurrency, isStream, streamStarted) +} + +// acquireAccountSlotWithWait acquires an account concurrency slot, waiting if necessary +// For streaming requests, sends ping events during the wait +// streamStarted is updated if streaming response has begun +func (h *GatewayHandler) acquireAccountSlotWithWait(c *gin.Context, account *model.Account, isStream bool, streamStarted *bool) (func(), error) { + ctx := c.Request.Context() + + // Try to acquire immediately + result, err := h.concurrencyService.AcquireAccountSlot(ctx, account.ID, account.Concurrency) + if err != nil { + return nil, err + } + + if result.Acquired { + return result.ReleaseFunc, nil + } + + // Need to wait - handle streaming ping if needed + return h.waitForSlotWithPing(c, "account", account.ID, account.Concurrency, isStream, streamStarted) +} + +// concurrencyError represents a concurrency limit error with context +type concurrencyError struct { + SlotType string + IsTimeout bool +} + +func (e *concurrencyError) Error() string { + if e.IsTimeout { + return fmt.Sprintf("timeout waiting for %s concurrency slot", e.SlotType) + } + return fmt.Sprintf("%s concurrency limit reached", e.SlotType) +} + +// waitForSlotWithPing waits for a concurrency slot, sending ping events for streaming requests +// Note: For streaming requests, we send ping to keep the connection alive. +// streamStarted pointer is updated when streaming begins (for proper error handling by caller) +func (h *GatewayHandler) waitForSlotWithPing(c *gin.Context, slotType string, id int64, maxConcurrency int, isStream bool, streamStarted *bool) (func(), error) { + ctx, cancel := context.WithTimeout(c.Request.Context(), maxConcurrencyWait) + defer cancel() + + // For streaming requests, set up SSE headers for ping + var flusher http.Flusher + if isStream { + var ok bool + flusher, ok = c.Writer.(http.Flusher) + if !ok { + return nil, fmt.Errorf("streaming not supported") + } + } + + pingTicker := time.NewTicker(pingInterval) + defer pingTicker.Stop() + + pollTicker := time.NewTicker(100 * time.Millisecond) + defer pollTicker.Stop() + + for { + select { + case <-ctx.Done(): + return nil, &concurrencyError{ + SlotType: slotType, + IsTimeout: true, + } + + case <-pingTicker.C: + // Send ping for streaming requests to keep connection alive + if isStream && flusher != nil { + // Set headers on first ping (lazy initialization) + if !*streamStarted { + c.Header("Content-Type", "text/event-stream") + c.Header("Cache-Control", "no-cache") + c.Header("Connection", "keep-alive") + c.Header("X-Accel-Buffering", "no") + *streamStarted = true + } + fmt.Fprintf(c.Writer, "data: {\"type\": \"ping\"}\n\n") + flusher.Flush() + } + + case <-pollTicker.C: + // Try to acquire slot + var result *service.AcquireResult + var err error + + if slotType == "user" { + result, err = h.concurrencyService.AcquireUserSlot(ctx, id, maxConcurrency) + } else { + result, err = h.concurrencyService.AcquireAccountSlot(ctx, id, maxConcurrency) + } + + if err != nil { + return nil, err + } + + if result.Acquired { + return result.ReleaseFunc, nil + } + } + } +} + +// Models handles listing available models +// GET /v1/models +func (h *GatewayHandler) Models(c *gin.Context) { + models := []gin.H{ + { + "id": "claude-opus-4-5-20251101", + "type": "model", + "display_name": "Claude Opus 4.5", + "created_at": "2025-11-01T00:00:00Z", + }, + { + "id": "claude-sonnet-4-5-20250929", + "type": "model", + "display_name": "Claude Sonnet 4.5", + "created_at": "2025-09-29T00:00:00Z", + }, + { + "id": "claude-haiku-4-5-20251001", + "type": "model", + "display_name": "Claude Haiku 4.5", + "created_at": "2025-10-01T00:00:00Z", + }, + } + + c.JSON(http.StatusOK, gin.H{ + "data": models, + "object": "list", + }) +} + +// Usage handles getting account balance for CC Switch integration +// GET /v1/usage +func (h *GatewayHandler) Usage(c *gin.Context) { + apiKey, ok := middleware.GetApiKeyFromContext(c) + if !ok { + h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key") + return + } + + user, ok := middleware.GetUserFromContext(c) + if !ok { + h.errorResponse(c, http.StatusUnauthorized, "authentication_error", "Invalid API key") + return + } + + // 订阅模式:返回订阅限额信息 + if apiKey.Group != nil && apiKey.Group.IsSubscriptionType() { + subscription, ok := middleware.GetSubscriptionFromContext(c) + if !ok { + h.errorResponse(c, http.StatusForbidden, "subscription_error", "No active subscription") + return + } + + remaining := h.calculateSubscriptionRemaining(apiKey.Group, subscription) + c.JSON(http.StatusOK, gin.H{ + "isValid": true, + "planName": apiKey.Group.Name, + "remaining": remaining, + "unit": "USD", + }) + return + } + + // 余额模式:返回钱包余额 + latestUser, err := h.userService.GetByID(c.Request.Context(), user.ID) + if err != nil { + h.errorResponse(c, http.StatusInternalServerError, "api_error", "Failed to get user info") + return + } + + c.JSON(http.StatusOK, gin.H{ + "isValid": true, + "planName": "钱包余额", + "remaining": latestUser.Balance, + "unit": "USD", + }) +} + +// calculateSubscriptionRemaining 计算订阅剩余可用额度 +// 逻辑: +// 1. 如果日/周/月任一限额达到100%,返回0 +// 2. 否则返回所有已配置周期中剩余额度的最小值 +func (h *GatewayHandler) calculateSubscriptionRemaining(group *model.Group, sub *model.UserSubscription) float64 { + var remainingValues []float64 + + // 检查日限额 + if group.HasDailyLimit() { + remaining := *group.DailyLimitUSD - sub.DailyUsageUSD + if remaining <= 0 { + return 0 + } + remainingValues = append(remainingValues, remaining) + } + + // 检查周限额 + if group.HasWeeklyLimit() { + remaining := *group.WeeklyLimitUSD - sub.WeeklyUsageUSD + if remaining <= 0 { + return 0 + } + remainingValues = append(remainingValues, remaining) + } + + // 检查月限额 + if group.HasMonthlyLimit() { + remaining := *group.MonthlyLimitUSD - sub.MonthlyUsageUSD + if remaining <= 0 { + return 0 + } + remainingValues = append(remainingValues, remaining) + } + + // 如果没有配置任何限额,返回-1表示无限制 + if len(remainingValues) == 0 { + return -1 + } + + // 返回最小值 + min := remainingValues[0] + for _, v := range remainingValues[1:] { + if v < min { + min = v + } + } + return min +} + +// handleConcurrencyError handles concurrency-related errors with proper 429 response +func (h *GatewayHandler) handleConcurrencyError(c *gin.Context, err error, slotType string, streamStarted bool) { + h.handleStreamingAwareError(c, http.StatusTooManyRequests, "rate_limit_error", + fmt.Sprintf("Concurrency limit exceeded for %s, please retry later", slotType), streamStarted) +} + +// handleStreamingAwareError handles errors that may occur after streaming has started +func (h *GatewayHandler) handleStreamingAwareError(c *gin.Context, status int, errType, message string, streamStarted bool) { + if streamStarted { + // Stream already started, send error as SSE event then close + flusher, ok := c.Writer.(http.Flusher) + if ok { + // Send error event in SSE format + errorEvent := fmt.Sprintf(`data: {"type": "error", "error": {"type": "%s", "message": "%s"}}`+"\n\n", errType, message) + fmt.Fprint(c.Writer, errorEvent) + flusher.Flush() + } + return + } + + // Normal case: return JSON response with proper status code + h.errorResponse(c, status, errType, message) +} + +// errorResponse 返回Claude API格式的错误响应 +func (h *GatewayHandler) errorResponse(c *gin.Context, status int, errType, message string) { + c.JSON(status, gin.H{ + "type": "error", + "error": gin.H{ + "type": errType, + "message": message, + }, + }) +} diff --git a/backend/internal/handler/handler.go b/backend/internal/handler/handler.go new file mode 100644 index 00000000..911a2434 --- /dev/null +++ b/backend/internal/handler/handler.go @@ -0,0 +1,70 @@ +package handler + +import ( + "sub2api/internal/handler/admin" + "sub2api/internal/repository" + "sub2api/internal/service" + + "github.com/redis/go-redis/v9" +) + +// AdminHandlers contains all admin-related HTTP handlers +type AdminHandlers struct { + Dashboard *admin.DashboardHandler + User *admin.UserHandler + Group *admin.GroupHandler + Account *admin.AccountHandler + OAuth *admin.OAuthHandler + Proxy *admin.ProxyHandler + Redeem *admin.RedeemHandler + Setting *admin.SettingHandler + System *admin.SystemHandler + Subscription *admin.SubscriptionHandler + Usage *admin.UsageHandler +} + +// Handlers contains all HTTP handlers +type Handlers struct { + Auth *AuthHandler + User *UserHandler + APIKey *APIKeyHandler + Usage *UsageHandler + Redeem *RedeemHandler + Subscription *SubscriptionHandler + Admin *AdminHandlers + Gateway *GatewayHandler + Setting *SettingHandler +} + +// BuildInfo contains build-time information +type BuildInfo struct { + Version string + BuildType string // "source" for manual builds, "release" for CI builds +} + +// NewHandlers creates a new Handlers instance with all handlers initialized +func NewHandlers(services *service.Services, repos *repository.Repositories, rdb *redis.Client, buildInfo BuildInfo) *Handlers { + return &Handlers{ + Auth: NewAuthHandler(services.Auth), + User: NewUserHandler(services.User), + APIKey: NewAPIKeyHandler(services.ApiKey), + Usage: NewUsageHandler(services.Usage, repos.UsageLog, services.ApiKey), + Redeem: NewRedeemHandler(services.Redeem), + Subscription: NewSubscriptionHandler(services.Subscription), + Admin: &AdminHandlers{ + Dashboard: admin.NewDashboardHandler(services.Admin, repos.UsageLog), + User: admin.NewUserHandler(services.Admin), + Group: admin.NewGroupHandler(services.Admin), + Account: admin.NewAccountHandler(services.Admin, services.OAuth, services.RateLimit, services.AccountUsage, services.AccountTest), + OAuth: admin.NewOAuthHandler(services.OAuth, services.Admin), + Proxy: admin.NewProxyHandler(services.Admin), + Redeem: admin.NewRedeemHandler(services.Admin), + Setting: admin.NewSettingHandler(services.Setting, services.Email), + System: admin.NewSystemHandler(rdb, buildInfo.Version, buildInfo.BuildType), + Subscription: admin.NewSubscriptionHandler(services.Subscription), + Usage: admin.NewUsageHandler(repos.UsageLog, repos.ApiKey, services.Usage, services.Admin), + }, + Gateway: NewGatewayHandler(services.Gateway, services.User, services.Concurrency, services.BillingCache), + Setting: NewSettingHandler(services.Setting, buildInfo.Version), + } +} diff --git a/backend/internal/handler/redeem_handler.go b/backend/internal/handler/redeem_handler.go new file mode 100644 index 00000000..2e8c8133 --- /dev/null +++ b/backend/internal/handler/redeem_handler.go @@ -0,0 +1,92 @@ +package handler + +import ( + "sub2api/internal/model" + "sub2api/internal/pkg/response" + "sub2api/internal/service" + + "github.com/gin-gonic/gin" +) + +// RedeemHandler handles redeem code-related requests +type RedeemHandler struct { + redeemService *service.RedeemService +} + +// NewRedeemHandler creates a new RedeemHandler +func NewRedeemHandler(redeemService *service.RedeemService) *RedeemHandler { + return &RedeemHandler{ + redeemService: redeemService, + } +} + +// RedeemRequest represents the redeem code request payload +type RedeemRequest struct { + Code string `json:"code" binding:"required"` +} + +// RedeemResponse represents the redeem response +type RedeemResponse struct { + Message string `json:"message"` + Type string `json:"type"` + Value float64 `json:"value"` + NewBalance *float64 `json:"new_balance,omitempty"` + NewConcurrency *int `json:"new_concurrency,omitempty"` +} + +// Redeem handles redeeming a code +// POST /api/v1/redeem +func (h *RedeemHandler) Redeem(c *gin.Context) { + userValue, exists := c.Get("user") + if !exists { + response.Unauthorized(c, "User not authenticated") + return + } + + user, ok := userValue.(*model.User) + if !ok { + response.InternalError(c, "Invalid user context") + return + } + + var req RedeemRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + result, err := h.redeemService.Redeem(c.Request.Context(), user.ID, req.Code) + if err != nil { + response.BadRequest(c, "Failed to redeem code: "+err.Error()) + return + } + + response.Success(c, result) +} + +// GetHistory returns the user's redemption history +// GET /api/v1/redeem/history +func (h *RedeemHandler) GetHistory(c *gin.Context) { + userValue, exists := c.Get("user") + if !exists { + response.Unauthorized(c, "User not authenticated") + return + } + + user, ok := userValue.(*model.User) + if !ok { + response.InternalError(c, "Invalid user context") + return + } + + // Default limit is 25 + limit := 25 + + codes, err := h.redeemService.GetUserHistory(c.Request.Context(), user.ID, limit) + if err != nil { + response.InternalError(c, "Failed to get history: "+err.Error()) + return + } + + response.Success(c, codes) +} diff --git a/backend/internal/handler/setting_handler.go b/backend/internal/handler/setting_handler.go new file mode 100644 index 00000000..f29e4569 --- /dev/null +++ b/backend/internal/handler/setting_handler.go @@ -0,0 +1,35 @@ +package handler + +import ( + "sub2api/internal/pkg/response" + "sub2api/internal/service" + + "github.com/gin-gonic/gin" +) + +// SettingHandler 公开设置处理器(无需认证) +type SettingHandler struct { + settingService *service.SettingService + version string +} + +// NewSettingHandler 创建公开设置处理器 +func NewSettingHandler(settingService *service.SettingService, version string) *SettingHandler { + return &SettingHandler{ + settingService: settingService, + version: version, + } +} + +// GetPublicSettings 获取公开设置 +// GET /api/v1/settings/public +func (h *SettingHandler) GetPublicSettings(c *gin.Context) { + settings, err := h.settingService.GetPublicSettings(c.Request.Context()) + if err != nil { + response.InternalError(c, "Failed to get settings: "+err.Error()) + return + } + + settings.Version = h.version + response.Success(c, settings) +} diff --git a/backend/internal/handler/subscription_handler.go b/backend/internal/handler/subscription_handler.go new file mode 100644 index 00000000..c80f7980 --- /dev/null +++ b/backend/internal/handler/subscription_handler.go @@ -0,0 +1,203 @@ +package handler + +import ( + "sub2api/internal/model" + "sub2api/internal/pkg/response" + "sub2api/internal/service" + + "github.com/gin-gonic/gin" +) + +// SubscriptionSummaryItem represents a subscription item in summary +type SubscriptionSummaryItem struct { + ID int64 `json:"id"` + GroupID int64 `json:"group_id"` + GroupName string `json:"group_name"` + Status string `json:"status"` + DailyUsedUSD float64 `json:"daily_used_usd,omitempty"` + DailyLimitUSD float64 `json:"daily_limit_usd,omitempty"` + WeeklyUsedUSD float64 `json:"weekly_used_usd,omitempty"` + WeeklyLimitUSD float64 `json:"weekly_limit_usd,omitempty"` + MonthlyUsedUSD float64 `json:"monthly_used_usd,omitempty"` + MonthlyLimitUSD float64 `json:"monthly_limit_usd,omitempty"` + ExpiresAt *string `json:"expires_at,omitempty"` +} + +// SubscriptionProgressInfo represents subscription with progress info +type SubscriptionProgressInfo struct { + Subscription *model.UserSubscription `json:"subscription"` + Progress *service.SubscriptionProgress `json:"progress"` +} + +// SubscriptionHandler handles user subscription operations +type SubscriptionHandler struct { + subscriptionService *service.SubscriptionService +} + +// NewSubscriptionHandler creates a new user subscription handler +func NewSubscriptionHandler(subscriptionService *service.SubscriptionService) *SubscriptionHandler { + return &SubscriptionHandler{ + subscriptionService: subscriptionService, + } +} + +// List handles listing current user's subscriptions +// GET /api/v1/subscriptions +func (h *SubscriptionHandler) List(c *gin.Context) { + user, exists := c.Get("user") + if !exists { + response.Unauthorized(c, "User not found in context") + return + } + + u, ok := user.(*model.User) + if !ok { + response.InternalError(c, "Invalid user in context") + return + } + + subscriptions, err := h.subscriptionService.ListUserSubscriptions(c.Request.Context(), u.ID) + if err != nil { + response.InternalError(c, "Failed to list subscriptions: "+err.Error()) + return + } + + response.Success(c, subscriptions) +} + +// GetActive handles getting current user's active subscriptions +// GET /api/v1/subscriptions/active +func (h *SubscriptionHandler) GetActive(c *gin.Context) { + user, exists := c.Get("user") + if !exists { + response.Unauthorized(c, "User not found in context") + return + } + + u, ok := user.(*model.User) + if !ok { + response.InternalError(c, "Invalid user in context") + return + } + + subscriptions, err := h.subscriptionService.ListActiveUserSubscriptions(c.Request.Context(), u.ID) + if err != nil { + response.InternalError(c, "Failed to get active subscriptions: "+err.Error()) + return + } + + response.Success(c, subscriptions) +} + +// GetProgress handles getting subscription progress for current user +// GET /api/v1/subscriptions/progress +func (h *SubscriptionHandler) GetProgress(c *gin.Context) { + user, exists := c.Get("user") + if !exists { + response.Unauthorized(c, "User not found in context") + return + } + + u, ok := user.(*model.User) + if !ok { + response.InternalError(c, "Invalid user in context") + return + } + + // Get all active subscriptions with progress + subscriptions, err := h.subscriptionService.ListActiveUserSubscriptions(c.Request.Context(), u.ID) + if err != nil { + response.InternalError(c, "Failed to get subscriptions: "+err.Error()) + return + } + + result := make([]SubscriptionProgressInfo, 0, len(subscriptions)) + for i := range subscriptions { + sub := &subscriptions[i] + progress, err := h.subscriptionService.GetSubscriptionProgress(c.Request.Context(), sub.ID) + if err != nil { + // Skip subscriptions with errors + continue + } + result = append(result, SubscriptionProgressInfo{ + Subscription: sub, + Progress: progress, + }) + } + + response.Success(c, result) +} + +// GetSummary handles getting a summary of current user's subscription status +// GET /api/v1/subscriptions/summary +func (h *SubscriptionHandler) GetSummary(c *gin.Context) { + user, exists := c.Get("user") + if !exists { + response.Unauthorized(c, "User not found in context") + return + } + + u, ok := user.(*model.User) + if !ok { + response.InternalError(c, "Invalid user in context") + return + } + + // Get all active subscriptions + subscriptions, err := h.subscriptionService.ListActiveUserSubscriptions(c.Request.Context(), u.ID) + if err != nil { + response.InternalError(c, "Failed to get subscriptions: "+err.Error()) + return + } + + var totalUsed float64 + items := make([]SubscriptionSummaryItem, 0, len(subscriptions)) + + for _, sub := range subscriptions { + item := SubscriptionSummaryItem{ + ID: sub.ID, + GroupID: sub.GroupID, + Status: sub.Status, + DailyUsedUSD: sub.DailyUsageUSD, + WeeklyUsedUSD: sub.WeeklyUsageUSD, + MonthlyUsedUSD: sub.MonthlyUsageUSD, + } + + // Add group info if preloaded + if sub.Group != nil { + item.GroupName = sub.Group.Name + if sub.Group.DailyLimitUSD != nil { + item.DailyLimitUSD = *sub.Group.DailyLimitUSD + } + if sub.Group.WeeklyLimitUSD != nil { + item.WeeklyLimitUSD = *sub.Group.WeeklyLimitUSD + } + if sub.Group.MonthlyLimitUSD != nil { + item.MonthlyLimitUSD = *sub.Group.MonthlyLimitUSD + } + } + + // Format expiration time + if !sub.ExpiresAt.IsZero() { + formatted := sub.ExpiresAt.Format("2006-01-02T15:04:05Z07:00") + item.ExpiresAt = &formatted + } + + // Track total usage (use monthly as the most comprehensive) + totalUsed += sub.MonthlyUsageUSD + + items = append(items, item) + } + + summary := struct { + ActiveCount int `json:"active_count"` + TotalUsedUSD float64 `json:"total_used_usd"` + Subscriptions []SubscriptionSummaryItem `json:"subscriptions"` + }{ + ActiveCount: len(subscriptions), + TotalUsedUSD: totalUsed, + Subscriptions: items, + } + + response.Success(c, summary) +} diff --git a/backend/internal/handler/usage_handler.go b/backend/internal/handler/usage_handler.go new file mode 100644 index 00000000..1a6f2614 --- /dev/null +++ b/backend/internal/handler/usage_handler.go @@ -0,0 +1,396 @@ +package handler + +import ( + "strconv" + "time" + + "sub2api/internal/model" + "sub2api/internal/pkg/response" + "sub2api/internal/pkg/timezone" + "sub2api/internal/repository" + "sub2api/internal/service" + + "github.com/gin-gonic/gin" +) + +// UsageHandler handles usage-related requests +type UsageHandler struct { + usageService *service.UsageService + usageRepo *repository.UsageLogRepository + apiKeyService *service.ApiKeyService +} + +// NewUsageHandler creates a new UsageHandler +func NewUsageHandler(usageService *service.UsageService, usageRepo *repository.UsageLogRepository, apiKeyService *service.ApiKeyService) *UsageHandler { + return &UsageHandler{ + usageService: usageService, + usageRepo: usageRepo, + apiKeyService: apiKeyService, + } +} + +// List handles listing usage records with pagination +// GET /api/v1/usage +func (h *UsageHandler) List(c *gin.Context) { + userValue, exists := c.Get("user") + if !exists { + response.Unauthorized(c, "User not authenticated") + return + } + + user, ok := userValue.(*model.User) + if !ok { + response.InternalError(c, "Invalid user context") + return + } + + page, pageSize := response.ParsePagination(c) + + var apiKeyID int64 + if apiKeyIDStr := c.Query("api_key_id"); apiKeyIDStr != "" { + id, err := strconv.ParseInt(apiKeyIDStr, 10, 64) + if err != nil { + response.BadRequest(c, "Invalid api_key_id") + return + } + + // [Security Fix] Verify API Key ownership to prevent horizontal privilege escalation + apiKey, err := h.apiKeyService.GetByID(c.Request.Context(), id) + if err != nil { + response.NotFound(c, "API key not found") + return + } + if apiKey.UserID != user.ID { + response.Forbidden(c, "Not authorized to access this API key's usage records") + return + } + + apiKeyID = id + } + + params := repository.PaginationParams{Page: page, PageSize: pageSize} + var records []model.UsageLog + var result *repository.PaginationResult + var err error + + if apiKeyID > 0 { + records, result, err = h.usageService.ListByApiKey(c.Request.Context(), apiKeyID, params) + } else { + records, result, err = h.usageService.ListByUser(c.Request.Context(), user.ID, params) + } + if err != nil { + response.InternalError(c, "Failed to list usage records: "+err.Error()) + return + } + + response.Paginated(c, records, result.Total, page, pageSize) +} + +// GetByID handles getting a single usage record +// GET /api/v1/usage/:id +func (h *UsageHandler) GetByID(c *gin.Context) { + userValue, exists := c.Get("user") + if !exists { + response.Unauthorized(c, "User not authenticated") + return + } + + user, ok := userValue.(*model.User) + if !ok { + response.InternalError(c, "Invalid user context") + return + } + + usageID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid usage ID") + return + } + + record, err := h.usageService.GetByID(c.Request.Context(), usageID) + if err != nil { + response.NotFound(c, "Usage record not found") + return + } + + // 验证所有权 + if record.UserID != user.ID { + response.Forbidden(c, "Not authorized to access this record") + return + } + + response.Success(c, record) +} + +// Stats handles getting usage statistics +// GET /api/v1/usage/stats +func (h *UsageHandler) Stats(c *gin.Context) { + userValue, exists := c.Get("user") + if !exists { + response.Unauthorized(c, "User not authenticated") + return + } + + user, ok := userValue.(*model.User) + if !ok { + response.InternalError(c, "Invalid user context") + return + } + + var apiKeyID int64 + if apiKeyIDStr := c.Query("api_key_id"); apiKeyIDStr != "" { + id, err := strconv.ParseInt(apiKeyIDStr, 10, 64) + if err != nil { + response.BadRequest(c, "Invalid api_key_id") + return + } + + // [Security Fix] Verify API Key ownership to prevent horizontal privilege escalation + apiKey, err := h.apiKeyService.GetByID(c.Request.Context(), id) + if err != nil { + response.NotFound(c, "API key not found") + return + } + if apiKey.UserID != user.ID { + response.Forbidden(c, "Not authorized to access this API key's statistics") + return + } + + apiKeyID = id + } + + // 获取时间范围参数 + now := timezone.Now() + var startTime, endTime time.Time + + // 优先使用 start_date 和 end_date 参数 + startDateStr := c.Query("start_date") + endDateStr := c.Query("end_date") + + if startDateStr != "" && endDateStr != "" { + // 使用自定义日期范围 + var err error + startTime, err = timezone.ParseInLocation("2006-01-02", startDateStr) + if err != nil { + response.BadRequest(c, "Invalid start_date format, use YYYY-MM-DD") + return + } + endTime, err = timezone.ParseInLocation("2006-01-02", endDateStr) + if err != nil { + response.BadRequest(c, "Invalid end_date format, use YYYY-MM-DD") + return + } + // 设置结束时间为当天结束 + endTime = endTime.Add(24*time.Hour - time.Nanosecond) + } else { + // 使用 period 参数 + period := c.DefaultQuery("period", "today") + switch period { + case "today": + startTime = timezone.StartOfDay(now) + case "week": + startTime = now.AddDate(0, 0, -7) + case "month": + startTime = now.AddDate(0, -1, 0) + default: + startTime = timezone.StartOfDay(now) + } + endTime = now + } + + var stats *service.UsageStats + var err error + if apiKeyID > 0 { + stats, err = h.usageService.GetStatsByApiKey(c.Request.Context(), apiKeyID, startTime, endTime) + } else { + stats, err = h.usageService.GetStatsByUser(c.Request.Context(), user.ID, startTime, endTime) + } + if err != nil { + response.InternalError(c, "Failed to get usage statistics: "+err.Error()) + return + } + + response.Success(c, stats) +} + +// parseUserTimeRange parses start_date, end_date query parameters for user dashboard +func parseUserTimeRange(c *gin.Context) (time.Time, time.Time) { + now := timezone.Now() + startDate := c.Query("start_date") + endDate := c.Query("end_date") + + var startTime, endTime time.Time + + if startDate != "" { + if t, err := timezone.ParseInLocation("2006-01-02", startDate); err == nil { + startTime = t + } else { + startTime = timezone.StartOfDay(now.AddDate(0, 0, -7)) + } + } else { + startTime = timezone.StartOfDay(now.AddDate(0, 0, -7)) + } + + if endDate != "" { + if t, err := timezone.ParseInLocation("2006-01-02", endDate); err == nil { + endTime = t.Add(24 * time.Hour) // Include the end date + } else { + endTime = timezone.StartOfDay(now.AddDate(0, 0, 1)) + } + } else { + endTime = timezone.StartOfDay(now.AddDate(0, 0, 1)) + } + + return startTime, endTime +} + +// DashboardStats handles getting user dashboard statistics +// GET /api/v1/usage/dashboard/stats +func (h *UsageHandler) DashboardStats(c *gin.Context) { + userValue, exists := c.Get("user") + if !exists { + response.Unauthorized(c, "User not authenticated") + return + } + + user, ok := userValue.(*model.User) + if !ok { + response.InternalError(c, "Invalid user context") + return + } + + stats, err := h.usageRepo.GetUserDashboardStats(c.Request.Context(), user.ID) + if err != nil { + response.InternalError(c, "Failed to get dashboard statistics") + return + } + + response.Success(c, stats) +} + +// DashboardTrend handles getting user usage trend data +// GET /api/v1/usage/dashboard/trend +func (h *UsageHandler) DashboardTrend(c *gin.Context) { + userValue, exists := c.Get("user") + if !exists { + response.Unauthorized(c, "User not authenticated") + return + } + + user, ok := userValue.(*model.User) + if !ok { + response.InternalError(c, "Invalid user context") + return + } + + startTime, endTime := parseUserTimeRange(c) + granularity := c.DefaultQuery("granularity", "day") + + trend, err := h.usageRepo.GetUserUsageTrendByUserID(c.Request.Context(), user.ID, startTime, endTime, granularity) + if err != nil { + response.InternalError(c, "Failed to get usage trend") + return + } + + response.Success(c, gin.H{ + "trend": trend, + "start_date": startTime.Format("2006-01-02"), + "end_date": endTime.Add(-24 * time.Hour).Format("2006-01-02"), + "granularity": granularity, + }) +} + +// DashboardModels handles getting user model usage statistics +// GET /api/v1/usage/dashboard/models +func (h *UsageHandler) DashboardModels(c *gin.Context) { + userValue, exists := c.Get("user") + if !exists { + response.Unauthorized(c, "User not authenticated") + return + } + + user, ok := userValue.(*model.User) + if !ok { + response.InternalError(c, "Invalid user context") + return + } + + startTime, endTime := parseUserTimeRange(c) + + stats, err := h.usageRepo.GetUserModelStats(c.Request.Context(), user.ID, startTime, endTime) + if err != nil { + response.InternalError(c, "Failed to get model statistics") + return + } + + response.Success(c, gin.H{ + "models": stats, + "start_date": startTime.Format("2006-01-02"), + "end_date": endTime.Add(-24 * time.Hour).Format("2006-01-02"), + }) +} + +// BatchApiKeysUsageRequest represents the request for batch API keys usage +type BatchApiKeysUsageRequest struct { + ApiKeyIDs []int64 `json:"api_key_ids" binding:"required"` +} + +// DashboardApiKeysUsage handles getting usage stats for user's own API keys +// POST /api/v1/usage/dashboard/api-keys-usage +func (h *UsageHandler) DashboardApiKeysUsage(c *gin.Context) { + userValue, exists := c.Get("user") + if !exists { + response.Unauthorized(c, "User not authenticated") + return + } + + user, ok := userValue.(*model.User) + if !ok { + response.InternalError(c, "Invalid user context") + return + } + + var req BatchApiKeysUsageRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + if len(req.ApiKeyIDs) == 0 { + response.Success(c, gin.H{"stats": map[string]interface{}{}}) + return + } + + // Verify ownership of all requested API keys + userApiKeys, _, err := h.apiKeyService.List(c.Request.Context(), user.ID, repository.PaginationParams{Page: 1, PageSize: 1000}) + if err != nil { + response.InternalError(c, "Failed to verify API key ownership") + return + } + + userApiKeyIDs := make(map[int64]bool) + for _, key := range userApiKeys { + userApiKeyIDs[key.ID] = true + } + + // Filter to only include user's own API keys + validApiKeyIDs := make([]int64, 0) + for _, id := range req.ApiKeyIDs { + if userApiKeyIDs[id] { + validApiKeyIDs = append(validApiKeyIDs, id) + } + } + + if len(validApiKeyIDs) == 0 { + response.Success(c, gin.H{"stats": map[string]interface{}{}}) + return + } + + stats, err := h.usageRepo.GetBatchApiKeyUsageStats(c.Request.Context(), validApiKeyIDs) + if err != nil { + response.InternalError(c, "Failed to get API key usage stats") + return + } + + response.Success(c, gin.H{"stats": stats}) +} diff --git a/backend/internal/handler/user_handler.go b/backend/internal/handler/user_handler.go new file mode 100644 index 00000000..7a49a0bd --- /dev/null +++ b/backend/internal/handler/user_handler.go @@ -0,0 +1,85 @@ +package handler + +import ( + "sub2api/internal/model" + "sub2api/internal/pkg/response" + "sub2api/internal/service" + + "github.com/gin-gonic/gin" +) + +// UserHandler handles user-related requests +type UserHandler struct { + userService *service.UserService +} + +// NewUserHandler creates a new UserHandler +func NewUserHandler(userService *service.UserService) *UserHandler { + return &UserHandler{ + userService: userService, + } +} + +// ChangePasswordRequest represents the change password request payload +type ChangePasswordRequest struct { + OldPassword string `json:"old_password" binding:"required"` + NewPassword string `json:"new_password" binding:"required,min=6"` +} + +// GetProfile handles getting user profile +// GET /api/v1/users/me +func (h *UserHandler) GetProfile(c *gin.Context) { + userValue, exists := c.Get("user") + if !exists { + response.Unauthorized(c, "User not authenticated") + return + } + + user, ok := userValue.(*model.User) + if !ok { + response.InternalError(c, "Invalid user context") + return + } + + userData, err := h.userService.GetByID(c.Request.Context(), user.ID) + if err != nil { + response.InternalError(c, "Failed to get user profile: "+err.Error()) + return + } + + response.Success(c, userData) +} + +// ChangePassword handles changing user password +// POST /api/v1/users/me/password +func (h *UserHandler) ChangePassword(c *gin.Context) { + userValue, exists := c.Get("user") + if !exists { + response.Unauthorized(c, "User not authenticated") + return + } + + user, ok := userValue.(*model.User) + if !ok { + response.InternalError(c, "Invalid user context") + return + } + + var req ChangePasswordRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + svcReq := service.ChangePasswordRequest{ + CurrentPassword: req.OldPassword, + NewPassword: req.NewPassword, + } + err := h.userService.ChangePassword(c.Request.Context(), user.ID, svcReq) + if err != nil { + response.BadRequest(c, "Failed to change password: "+err.Error()) + return + } + + response.Success(c, gin.H{"message": "Password changed successfully"}) +} diff --git a/backend/internal/middleware/admin_only.go b/backend/internal/middleware/admin_only.go new file mode 100644 index 00000000..f35471a3 --- /dev/null +++ b/backend/internal/middleware/admin_only.go @@ -0,0 +1,28 @@ +package middleware + +import ( + "sub2api/internal/model" + + "github.com/gin-gonic/gin" +) + +// AdminOnly 管理员权限中间件 +// 必须在JWTAuth中间件之后使用 +func AdminOnly() gin.HandlerFunc { + return func(c *gin.Context) { + // 从上下文获取用户 + user, exists := GetUserFromContext(c) + if !exists { + AbortWithError(c, 401, "UNAUTHORIZED", "User not found in context") + return + } + + // 检查是否为管理员 + if user.Role != model.RoleAdmin { + AbortWithError(c, 403, "FORBIDDEN", "Admin access required") + return + } + + c.Next() + } +} diff --git a/backend/internal/middleware/api_key_auth.go b/backend/internal/middleware/api_key_auth.go new file mode 100644 index 00000000..0c3e0922 --- /dev/null +++ b/backend/internal/middleware/api_key_auth.go @@ -0,0 +1,161 @@ +package middleware + +import ( + "context" + "errors" + "log" + "strings" + "sub2api/internal/model" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// ApiKeyAuthService 定义API Key认证服务需要的接口 +type ApiKeyAuthService interface { + GetByKey(ctx context.Context, key string) (*model.ApiKey, error) +} + +// SubscriptionAuthService 定义订阅认证服务需要的接口 +type SubscriptionAuthService interface { + GetActiveSubscription(ctx context.Context, userID, groupID int64) (*model.UserSubscription, error) + ValidateSubscription(ctx context.Context, sub *model.UserSubscription) error + CheckAndActivateWindow(ctx context.Context, sub *model.UserSubscription) error + CheckAndResetWindows(ctx context.Context, sub *model.UserSubscription) error + CheckUsageLimits(ctx context.Context, sub *model.UserSubscription, group *model.Group, additionalCost float64) error +} + +// ApiKeyAuth API Key认证中间件 +func ApiKeyAuth(apiKeyRepo ApiKeyAuthService) gin.HandlerFunc { + return ApiKeyAuthWithSubscription(apiKeyRepo, nil) +} + +// ApiKeyAuthWithSubscription API Key认证中间件(支持订阅验证) +func ApiKeyAuthWithSubscription(apiKeyRepo ApiKeyAuthService, subscriptionService SubscriptionAuthService) gin.HandlerFunc { + return func(c *gin.Context) { + // 尝试从Authorization header中提取API key (Bearer scheme) + authHeader := c.GetHeader("Authorization") + var apiKeyString string + + if authHeader != "" { + // 验证Bearer scheme + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) == 2 && parts[0] == "Bearer" { + apiKeyString = parts[1] + } + } + + // 如果Authorization header中没有,尝试从x-api-key header中提取 + if apiKeyString == "" { + apiKeyString = c.GetHeader("x-api-key") + } + + // 如果两个header都没有API key + if apiKeyString == "" { + AbortWithError(c, 401, "API_KEY_REQUIRED", "API key is required in Authorization header (Bearer scheme) or x-api-key header") + return + } + + // 从数据库验证API key + apiKey, err := apiKeyRepo.GetByKey(c.Request.Context(), apiKeyString) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + AbortWithError(c, 401, "INVALID_API_KEY", "Invalid API key") + return + } + AbortWithError(c, 500, "INTERNAL_ERROR", "Failed to validate API key") + return + } + + // 检查API key是否激活 + if !apiKey.IsActive() { + AbortWithError(c, 401, "API_KEY_DISABLED", "API key is disabled") + return + } + + // 检查关联的用户 + if apiKey.User == nil { + AbortWithError(c, 401, "USER_NOT_FOUND", "User associated with API key not found") + return + } + + // 检查用户状态 + if !apiKey.User.IsActive() { + AbortWithError(c, 401, "USER_INACTIVE", "User account is not active") + return + } + + // 判断计费方式:订阅模式 vs 余额模式 + isSubscriptionType := apiKey.Group != nil && apiKey.Group.IsSubscriptionType() + + if isSubscriptionType && subscriptionService != nil { + // 订阅模式:验证订阅 + subscription, err := subscriptionService.GetActiveSubscription( + c.Request.Context(), + apiKey.User.ID, + apiKey.Group.ID, + ) + if err != nil { + AbortWithError(c, 403, "SUBSCRIPTION_NOT_FOUND", "No active subscription found for this group") + return + } + + // 验证订阅状态(是否过期、暂停等) + if err := subscriptionService.ValidateSubscription(c.Request.Context(), subscription); err != nil { + AbortWithError(c, 403, "SUBSCRIPTION_INVALID", err.Error()) + return + } + + // 激活滑动窗口(首次使用时) + if err := subscriptionService.CheckAndActivateWindow(c.Request.Context(), subscription); err != nil { + log.Printf("Failed to activate subscription windows: %v", err) + } + + // 检查并重置过期窗口 + if err := subscriptionService.CheckAndResetWindows(c.Request.Context(), subscription); err != nil { + log.Printf("Failed to reset subscription windows: %v", err) + } + + // 预检查用量限制(使用0作为额外费用进行预检查) + if err := subscriptionService.CheckUsageLimits(c.Request.Context(), subscription, apiKey.Group, 0); err != nil { + AbortWithError(c, 429, "USAGE_LIMIT_EXCEEDED", err.Error()) + return + } + + // 将订阅信息存入上下文 + c.Set(string(ContextKeySubscription), subscription) + } else { + // 余额模式:检查用户余额 + if apiKey.User.Balance <= 0 { + AbortWithError(c, 403, "INSUFFICIENT_BALANCE", "Insufficient account balance") + return + } + } + + // 将API key和用户信息存入上下文 + c.Set(string(ContextKeyApiKey), apiKey) + c.Set(string(ContextKeyUser), apiKey.User) + + c.Next() + } +} + +// GetApiKeyFromContext 从上下文中获取API key +func GetApiKeyFromContext(c *gin.Context) (*model.ApiKey, bool) { + value, exists := c.Get(string(ContextKeyApiKey)) + if !exists { + return nil, false + } + apiKey, ok := value.(*model.ApiKey) + return apiKey, ok +} + +// GetSubscriptionFromContext 从上下文中获取订阅信息 +func GetSubscriptionFromContext(c *gin.Context) (*model.UserSubscription, bool) { + value, exists := c.Get(string(ContextKeySubscription)) + if !exists { + return nil, false + } + subscription, ok := value.(*model.UserSubscription) + return subscription, ok +} diff --git a/backend/internal/middleware/cors.go b/backend/internal/middleware/cors.go new file mode 100644 index 00000000..bc16279f --- /dev/null +++ b/backend/internal/middleware/cors.go @@ -0,0 +1,24 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" +) + +// CORS 跨域中间件 +func CORS() gin.HandlerFunc { + return func(c *gin.Context) { + // 设置允许跨域的响应头 + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With, X-API-Key") + c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH") + + // 处理预检请求 + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + + c.Next() + } +} diff --git a/backend/internal/middleware/jwt_auth.go b/backend/internal/middleware/jwt_auth.go new file mode 100644 index 00000000..2a23d7ab --- /dev/null +++ b/backend/internal/middleware/jwt_auth.go @@ -0,0 +1,76 @@ +package middleware + +import ( + "context" + "strings" + "sub2api/internal/model" + "sub2api/internal/service" + + "github.com/gin-gonic/gin" +) + +// JWTAuth JWT认证中间件 +func JWTAuth(authService *service.AuthService, userRepo interface { + GetByID(ctx context.Context, id int64) (*model.User, error) +}) gin.HandlerFunc { + return func(c *gin.Context) { + // 从Authorization header中提取token + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + AbortWithError(c, 401, "UNAUTHORIZED", "Authorization header is required") + return + } + + // 验证Bearer scheme + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || parts[0] != "Bearer" { + AbortWithError(c, 401, "INVALID_AUTH_HEADER", "Authorization header format must be 'Bearer {token}'") + return + } + + tokenString := parts[1] + if tokenString == "" { + AbortWithError(c, 401, "EMPTY_TOKEN", "Token cannot be empty") + return + } + + // 验证token + claims, err := authService.ValidateToken(tokenString) + if err != nil { + if err == service.ErrTokenExpired { + AbortWithError(c, 401, "TOKEN_EXPIRED", "Token has expired") + return + } + AbortWithError(c, 401, "INVALID_TOKEN", "Invalid token") + return + } + + // 从数据库获取最新的用户信息 + user, err := userRepo.GetByID(c.Request.Context(), claims.UserID) + if err != nil { + AbortWithError(c, 401, "USER_NOT_FOUND", "User not found") + return + } + + // 检查用户状态 + if !user.IsActive() { + AbortWithError(c, 401, "USER_INACTIVE", "User account is not active") + return + } + + // 将用户信息存入上下文 + c.Set(string(ContextKeyUser), user) + + c.Next() + } +} + +// GetUserFromContext 从上下文中获取用户 +func GetUserFromContext(c *gin.Context) (*model.User, bool) { + value, exists := c.Get(string(ContextKeyUser)) + if !exists { + return nil, false + } + user, ok := value.(*model.User) + return user, ok +} diff --git a/backend/internal/middleware/logger.go b/backend/internal/middleware/logger.go new file mode 100644 index 00000000..a9beeb40 --- /dev/null +++ b/backend/internal/middleware/logger.go @@ -0,0 +1,52 @@ +package middleware + +import ( + "log" + "time" + + "github.com/gin-gonic/gin" +) + +// Logger 请求日志中间件 +func Logger() gin.HandlerFunc { + return func(c *gin.Context) { + // 开始时间 + startTime := time.Now() + + // 处理请求 + c.Next() + + // 结束时间 + endTime := time.Now() + + // 执行时间 + latency := endTime.Sub(startTime) + + // 请求方法 + method := c.Request.Method + + // 请求路径 + path := c.Request.URL.Path + + // 状态码 + statusCode := c.Writer.Status() + + // 客户端IP + clientIP := c.ClientIP() + + // 日志格式: [时间] 状态码 | 延迟 | IP | 方法 路径 + log.Printf("[GIN] %v | %3d | %13v | %15s | %-7s %s", + endTime.Format("2006/01/02 - 15:04:05"), + statusCode, + latency, + clientIP, + method, + path, + ) + + // 如果有错误,额外记录错误信息 + if len(c.Errors) > 0 { + log.Printf("[GIN] Errors: %v", c.Errors.String()) + } + } +} diff --git a/backend/internal/middleware/middleware.go b/backend/internal/middleware/middleware.go new file mode 100644 index 00000000..aeda0387 --- /dev/null +++ b/backend/internal/middleware/middleware.go @@ -0,0 +1,35 @@ +package middleware + +import "github.com/gin-gonic/gin" + +// ContextKey 定义上下文键类型 +type ContextKey string + +const ( + // ContextKeyUser 用户上下文键 + ContextKeyUser ContextKey = "user" + // ContextKeyApiKey API密钥上下文键 + ContextKeyApiKey ContextKey = "api_key" + // ContextKeySubscription 订阅上下文键 + ContextKeySubscription ContextKey = "subscription" +) + +// ErrorResponse 标准错误响应结构 +type ErrorResponse struct { + Code string `json:"code"` + Message string `json:"message"` +} + +// NewErrorResponse 创建错误响应 +func NewErrorResponse(code, message string) ErrorResponse { + return ErrorResponse{ + Code: code, + Message: message, + } +} + +// AbortWithError 中断请求并返回JSON错误 +func AbortWithError(c *gin.Context, statusCode int, code, message string) { + c.JSON(statusCode, NewErrorResponse(code, message)) + c.Abort() +} diff --git a/backend/internal/model/account.go b/backend/internal/model/account.go new file mode 100644 index 00000000..e42c813f --- /dev/null +++ b/backend/internal/model/account.go @@ -0,0 +1,265 @@ +package model + +import ( + "database/sql/driver" + "encoding/json" + "errors" + "time" + + "gorm.io/gorm" +) + +// JSONB 用于存储JSONB数据 +type JSONB map[string]interface{} + +func (j JSONB) Value() (driver.Value, error) { + if j == nil { + return nil, nil + } + return json.Marshal(j) +} + +func (j *JSONB) Scan(value interface{}) error { + if value == nil { + *j = nil + return nil + } + bytes, ok := value.([]byte) + if !ok { + return errors.New("type assertion to []byte failed") + } + return json.Unmarshal(bytes, j) +} + +type Account struct { + ID int64 `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:100;not null" json:"name"` + Platform string `gorm:"size:50;not null" json:"platform"` // anthropic/openai/gemini + Type string `gorm:"size:20;not null" json:"type"` // oauth/apikey + Credentials JSONB `gorm:"type:jsonb;default:'{}'" json:"credentials"` // 凭证(加密存储) + Extra JSONB `gorm:"type:jsonb;default:'{}'" json:"extra"` // 扩展信息 + ProxyID *int64 `gorm:"index" json:"proxy_id"` + Concurrency int `gorm:"default:3;not null" json:"concurrency"` + Priority int `gorm:"default:50;not null" json:"priority"` // 1-100,越小越高 + Status string `gorm:"size:20;default:active;not null" json:"status"` // active/disabled/error + ErrorMessage string `gorm:"type:text" json:"error_message"` + LastUsedAt *time.Time `gorm:"index" json:"last_used_at"` + CreatedAt time.Time `gorm:"not null" json:"created_at"` + UpdatedAt time.Time `gorm:"not null" json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + // 调度控制 + Schedulable bool `gorm:"default:true;not null" json:"schedulable"` + + // 限流状态 (429) + RateLimitedAt *time.Time `gorm:"index" json:"rate_limited_at"` + RateLimitResetAt *time.Time `gorm:"index" json:"rate_limit_reset_at"` + + // 过载状态 (529) + OverloadUntil *time.Time `gorm:"index" json:"overload_until"` + + // 5小时时间窗口 + SessionWindowStart *time.Time `json:"session_window_start"` + SessionWindowEnd *time.Time `json:"session_window_end"` + SessionWindowStatus string `gorm:"size:20" json:"session_window_status"` // allowed/allowed_warning/rejected + + // 关联 + Proxy *Proxy `gorm:"foreignKey:ProxyID" json:"proxy,omitempty"` + AccountGroups []AccountGroup `gorm:"foreignKey:AccountID" json:"account_groups,omitempty"` + + // 虚拟字段 (不存储到数据库) + GroupIDs []int64 `gorm:"-" json:"group_ids,omitempty"` +} + +func (Account) TableName() string { + return "accounts" +} + +// IsActive 检查是否激活 +func (a *Account) IsActive() bool { + return a.Status == "active" +} + +// IsSchedulable 检查账号是否可调度 +func (a *Account) IsSchedulable() bool { + if !a.IsActive() || !a.Schedulable { + return false + } + now := time.Now() + if a.OverloadUntil != nil && now.Before(*a.OverloadUntil) { + return false + } + if a.RateLimitResetAt != nil && now.Before(*a.RateLimitResetAt) { + return false + } + return true +} + +// IsRateLimited 检查是否处于限流状态 +func (a *Account) IsRateLimited() bool { + if a.RateLimitResetAt == nil { + return false + } + return time.Now().Before(*a.RateLimitResetAt) +} + +// IsOverloaded 检查是否处于过载状态 +func (a *Account) IsOverloaded() bool { + if a.OverloadUntil == nil { + return false + } + return time.Now().Before(*a.OverloadUntil) +} + +// IsOAuth 检查是否为OAuth类型账号(包括oauth和setup-token) +func (a *Account) IsOAuth() bool { + return a.Type == AccountTypeOAuth || a.Type == AccountTypeSetupToken +} + +// CanGetUsage 检查账号是否可以获取usage信息(只有oauth类型可以,setup-token没有profile权限) +func (a *Account) CanGetUsage() bool { + return a.Type == AccountTypeOAuth +} + +// GetCredential 获取凭证字段 +func (a *Account) GetCredential(key string) string { + if a.Credentials == nil { + return "" + } + if v, ok := a.Credentials[key]; ok { + if s, ok := v.(string); ok { + return s + } + } + return "" +} + +// GetModelMapping 获取模型映射配置 +// 返回格式: map[请求模型名]实际模型名 +func (a *Account) GetModelMapping() map[string]string { + if a.Credentials == nil { + return nil + } + raw, ok := a.Credentials["model_mapping"] + if !ok || raw == nil { + return nil + } + // 处理map[string]interface{}类型 + if m, ok := raw.(map[string]interface{}); ok { + result := make(map[string]string) + for k, v := range m { + if s, ok := v.(string); ok { + result[k] = s + } + } + if len(result) > 0 { + return result + } + } + return nil +} + +// IsModelSupported 检查请求的模型是否被该账号支持 +// 如果没有设置模型映射,则支持所有模型 +func (a *Account) IsModelSupported(requestedModel string) bool { + mapping := a.GetModelMapping() + if mapping == nil || len(mapping) == 0 { + return true // 没有映射配置,支持所有模型 + } + _, exists := mapping[requestedModel] + return exists +} + +// GetMappedModel 获取映射后的实际模型名 +// 如果没有映射,返回原始模型名 +func (a *Account) GetMappedModel(requestedModel string) string { + mapping := a.GetModelMapping() + if mapping == nil || len(mapping) == 0 { + return requestedModel + } + if mappedModel, exists := mapping[requestedModel]; exists { + return mappedModel + } + return requestedModel +} + +// GetBaseURL 获取API基础URL(用于apikey类型账号) +func (a *Account) GetBaseURL() string { + if a.Type != AccountTypeApiKey { + return "" + } + baseURL := a.GetCredential("base_url") + if baseURL == "" { + return "https://api.anthropic.com" // 默认URL + } + return baseURL +} + +// GetExtraString 从Extra字段获取字符串值 +func (a *Account) GetExtraString(key string) string { + if a.Extra == nil { + return "" + } + if v, ok := a.Extra[key]; ok { + if s, ok := v.(string); ok { + return s + } + } + return "" +} + +// IsCustomErrorCodesEnabled 检查是否启用自定义错误码功能(仅适用于 apikey 类型) +func (a *Account) IsCustomErrorCodesEnabled() bool { + if a.Type != AccountTypeApiKey || a.Credentials == nil { + return false + } + if v, ok := a.Credentials["custom_error_codes_enabled"]; ok { + if enabled, ok := v.(bool); ok { + return enabled + } + } + return false +} + +// GetCustomErrorCodes 获取自定义错误码列表 +func (a *Account) GetCustomErrorCodes() []int { + if a.Credentials == nil { + return nil + } + raw, ok := a.Credentials["custom_error_codes"] + if !ok || raw == nil { + return nil + } + // 处理 []interface{} 类型(JSON反序列化后的格式) + if arr, ok := raw.([]interface{}); ok { + result := make([]int, 0, len(arr)) + for _, v := range arr { + // JSON 数字默认解析为 float64 + if f, ok := v.(float64); ok { + result = append(result, int(f)) + } + } + return result + } + return nil +} + +// ShouldHandleErrorCode 检查指定错误码是否应该被处理(停止调度/标记限流等) +// 如果未启用自定义错误码或列表为空,返回 true(使用默认策略) +// 如果启用且列表非空,只有在列表中的错误码才返回 true +func (a *Account) ShouldHandleErrorCode(statusCode int) bool { + if !a.IsCustomErrorCodesEnabled() { + return true // 未启用,使用默认策略 + } + codes := a.GetCustomErrorCodes() + if len(codes) == 0 { + return true // 启用但列表为空,fallback到默认策略 + } + // 检查是否在自定义列表中 + for _, code := range codes { + if code == statusCode { + return true + } + } + return false +} diff --git a/backend/internal/model/account_group.go b/backend/internal/model/account_group.go new file mode 100644 index 00000000..9f48b6ce --- /dev/null +++ b/backend/internal/model/account_group.go @@ -0,0 +1,20 @@ +package model + +import ( + "time" +) + +type AccountGroup struct { + AccountID int64 `gorm:"primaryKey" json:"account_id"` + GroupID int64 `gorm:"primaryKey" json:"group_id"` + Priority int `gorm:"default:50;not null" json:"priority"` // 分组内优先级 + CreatedAt time.Time `gorm:"not null" json:"created_at"` + + // 关联 + Account *Account `gorm:"foreignKey:AccountID" json:"account,omitempty"` + Group *Group `gorm:"foreignKey:GroupID" json:"group,omitempty"` +} + +func (AccountGroup) TableName() string { + return "account_groups" +} diff --git a/backend/internal/model/api_key.go b/backend/internal/model/api_key.go new file mode 100644 index 00000000..11017081 --- /dev/null +++ b/backend/internal/model/api_key.go @@ -0,0 +1,32 @@ +package model + +import ( + "time" + + "gorm.io/gorm" +) + +type ApiKey struct { + ID int64 `gorm:"primaryKey" json:"id"` + UserID int64 `gorm:"index;not null" json:"user_id"` + Key string `gorm:"uniqueIndex;size:128;not null" json:"key"` // sk-xxx + Name string `gorm:"size:100;not null" json:"name"` + GroupID *int64 `gorm:"index" json:"group_id"` + Status string `gorm:"size:20;default:active;not null" json:"status"` // active/disabled + CreatedAt time.Time `gorm:"not null" json:"created_at"` + UpdatedAt time.Time `gorm:"not null" json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + // 关联 + User *User `gorm:"foreignKey:UserID" json:"user,omitempty"` + Group *Group `gorm:"foreignKey:GroupID" json:"group,omitempty"` +} + +func (ApiKey) TableName() string { + return "api_keys" +} + +// IsActive 检查是否激活 +func (k *ApiKey) IsActive() bool { + return k.Status == "active" +} diff --git a/backend/internal/model/group.go b/backend/internal/model/group.go new file mode 100644 index 00000000..f02b2692 --- /dev/null +++ b/backend/internal/model/group.go @@ -0,0 +1,73 @@ +package model + +import ( + "time" + + "gorm.io/gorm" +) + +// 订阅类型常量 +const ( + SubscriptionTypeStandard = "standard" // 标准计费模式(按余额扣费) + SubscriptionTypeSubscription = "subscription" // 订阅模式(按限额控制) +) + +type Group struct { + ID int64 `gorm:"primaryKey" json:"id"` + Name string `gorm:"uniqueIndex;size:100;not null" json:"name"` + Description string `gorm:"type:text" json:"description"` + Platform string `gorm:"size:50;default:anthropic;not null" json:"platform"` // anthropic/openai/gemini + RateMultiplier float64 `gorm:"type:decimal(10,4);default:1.0;not null" json:"rate_multiplier"` + IsExclusive bool `gorm:"default:false;not null" json:"is_exclusive"` + Status string `gorm:"size:20;default:active;not null" json:"status"` // active/disabled + + // 订阅功能字段 + SubscriptionType string `gorm:"size:20;default:standard;not null" json:"subscription_type"` // standard/subscription + DailyLimitUSD *float64 `gorm:"type:decimal(20,8)" json:"daily_limit_usd"` + WeeklyLimitUSD *float64 `gorm:"type:decimal(20,8)" json:"weekly_limit_usd"` + MonthlyLimitUSD *float64 `gorm:"type:decimal(20,8)" json:"monthly_limit_usd"` + + CreatedAt time.Time `gorm:"not null" json:"created_at"` + UpdatedAt time.Time `gorm:"not null" json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + // 关联 + AccountGroups []AccountGroup `gorm:"foreignKey:GroupID" json:"account_groups,omitempty"` + + // 虚拟字段 (不存储到数据库) + AccountCount int64 `gorm:"-" json:"account_count,omitempty"` +} + +func (Group) TableName() string { + return "groups" +} + +// IsActive 检查是否激活 +func (g *Group) IsActive() bool { + return g.Status == "active" +} + +// IsSubscriptionType 检查是否为订阅类型分组 +func (g *Group) IsSubscriptionType() bool { + return g.SubscriptionType == SubscriptionTypeSubscription +} + +// IsFreeSubscription 检查是否为免费订阅(不扣余额但有限额) +func (g *Group) IsFreeSubscription() bool { + return g.IsSubscriptionType() && g.RateMultiplier == 0 +} + +// HasDailyLimit 检查是否有日限额 +func (g *Group) HasDailyLimit() bool { + return g.DailyLimitUSD != nil && *g.DailyLimitUSD > 0 +} + +// HasWeeklyLimit 检查是否有周限额 +func (g *Group) HasWeeklyLimit() bool { + return g.WeeklyLimitUSD != nil && *g.WeeklyLimitUSD > 0 +} + +// HasMonthlyLimit 检查是否有月限额 +func (g *Group) HasMonthlyLimit() bool { + return g.MonthlyLimitUSD != nil && *g.MonthlyLimitUSD > 0 +} diff --git a/backend/internal/model/model.go b/backend/internal/model/model.go new file mode 100644 index 00000000..203f552a --- /dev/null +++ b/backend/internal/model/model.go @@ -0,0 +1,64 @@ +package model + +import ( + "gorm.io/gorm" +) + +// AutoMigrate 自动迁移所有模型 +func AutoMigrate(db *gorm.DB) error { + return db.AutoMigrate( + &User{}, + &ApiKey{}, + &Group{}, + &Account{}, + &AccountGroup{}, + &Proxy{}, + &RedeemCode{}, + &UsageLog{}, + &Setting{}, + &UserSubscription{}, + ) +} + +// 状态常量 +const ( + StatusActive = "active" + StatusDisabled = "disabled" + StatusError = "error" + StatusUnused = "unused" + StatusUsed = "used" + StatusExpired = "expired" +) + +// 角色常量 +const ( + RoleAdmin = "admin" + RoleUser = "user" +) + +// 平台常量 +const ( + PlatformAnthropic = "anthropic" + PlatformOpenAI = "openai" + PlatformGemini = "gemini" +) + +// 账号类型常量 +const ( + AccountTypeOAuth = "oauth" // OAuth类型账号(full scope: profile + inference) + AccountTypeSetupToken = "setup-token" // Setup Token类型账号(inference only scope) + AccountTypeApiKey = "apikey" // API Key类型账号 +) + +// 卡密类型常量 +const ( + RedeemTypeBalance = "balance" + RedeemTypeConcurrency = "concurrency" + RedeemTypeSubscription = "subscription" +) + +// 管理员调整类型常量 +const ( + AdjustmentTypeAdminBalance = "admin_balance" // 管理员调整余额 + AdjustmentTypeAdminConcurrency = "admin_concurrency" // 管理员调整并发数 +) diff --git a/backend/internal/model/proxy.go b/backend/internal/model/proxy.go new file mode 100644 index 00000000..af27dbe6 --- /dev/null +++ b/backend/internal/model/proxy.go @@ -0,0 +1,45 @@ +package model + +import ( + "fmt" + "time" + + "gorm.io/gorm" +) + +type Proxy struct { + ID int64 `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:100;not null" json:"name"` + Protocol string `gorm:"size:20;not null" json:"protocol"` // http/https/socks5 + Host string `gorm:"size:255;not null" json:"host"` + Port int `gorm:"not null" json:"port"` + Username string `gorm:"size:100" json:"username"` + Password string `gorm:"size:100" json:"-"` + Status string `gorm:"size:20;default:active;not null" json:"status"` // active/disabled + CreatedAt time.Time `gorm:"not null" json:"created_at"` + UpdatedAt time.Time `gorm:"not null" json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` +} + +func (Proxy) TableName() string { + return "proxies" +} + +// IsActive 检查是否激活 +func (p *Proxy) IsActive() bool { + return p.Status == "active" +} + +// URL 返回代理URL +func (p *Proxy) URL() string { + if p.Username != "" && p.Password != "" { + return fmt.Sprintf("%s://%s:%s@%s:%d", p.Protocol, p.Username, p.Password, p.Host, p.Port) + } + return fmt.Sprintf("%s://%s:%d", p.Protocol, p.Host, p.Port) +} + +// ProxyWithAccountCount extends Proxy with account count information +type ProxyWithAccountCount struct { + Proxy + AccountCount int64 `json:"account_count"` +} diff --git a/backend/internal/model/redeem_code.go b/backend/internal/model/redeem_code.go new file mode 100644 index 00000000..725361c3 --- /dev/null +++ b/backend/internal/model/redeem_code.go @@ -0,0 +1,47 @@ +package model + +import ( + "crypto/rand" + "encoding/hex" + "time" +) + +type RedeemCode struct { + ID int64 `gorm:"primaryKey" json:"id"` + Code string `gorm:"uniqueIndex;size:32;not null" json:"code"` + Type string `gorm:"size:20;default:balance;not null" json:"type"` // balance/concurrency/subscription + Value float64 `gorm:"type:decimal(20,8);not null" json:"value"` // 面值(USD)或并发数或有效天数 + Status string `gorm:"size:20;default:unused;not null" json:"status"` // unused/used + UsedBy *int64 `gorm:"index" json:"used_by"` + UsedAt *time.Time `json:"used_at"` + CreatedAt time.Time `gorm:"not null" json:"created_at"` + + // 订阅类型专用字段 + GroupID *int64 `gorm:"index" json:"group_id"` // 订阅分组ID (仅subscription类型使用) + ValidityDays int `gorm:"default:30" json:"validity_days"` // 订阅有效天数 (仅subscription类型使用) + + // 关联 + User *User `gorm:"foreignKey:UsedBy" json:"user,omitempty"` + Group *Group `gorm:"foreignKey:GroupID" json:"group,omitempty"` +} + +func (RedeemCode) TableName() string { + return "redeem_codes" +} + +// IsUsed 检查是否已使用 +func (r *RedeemCode) IsUsed() bool { + return r.Status == "used" +} + +// CanUse 检查是否可以使用 +func (r *RedeemCode) CanUse() bool { + return r.Status == "unused" +} + +// GenerateRedeemCode 生成唯一的兑换码 +func GenerateRedeemCode() string { + b := make([]byte, 16) + rand.Read(b) + return hex.EncodeToString(b) +} diff --git a/backend/internal/model/setting.go b/backend/internal/model/setting.go new file mode 100644 index 00000000..0551a679 --- /dev/null +++ b/backend/internal/model/setting.go @@ -0,0 +1,95 @@ +package model + +import ( + "time" +) + +// Setting 系统设置模型(Key-Value存储) +type Setting struct { + ID int64 `gorm:"primaryKey" json:"id"` + Key string `gorm:"uniqueIndex;size:100;not null" json:"key"` + Value string `gorm:"type:text;not null" json:"value"` + UpdatedAt time.Time `gorm:"not null" json:"updated_at"` +} + +func (Setting) TableName() string { + return "settings" +} + +// 设置Key常量 +const ( + // 注册设置 + SettingKeyRegistrationEnabled = "registration_enabled" // 是否开放注册 + SettingKeyEmailVerifyEnabled = "email_verify_enabled" // 是否开启邮件验证 + + // 邮件服务设置 + SettingKeySmtpHost = "smtp_host" // SMTP服务器地址 + SettingKeySmtpPort = "smtp_port" // SMTP端口 + SettingKeySmtpUsername = "smtp_username" // SMTP用户名 + SettingKeySmtpPassword = "smtp_password" // SMTP密码(加密存储) + SettingKeySmtpFrom = "smtp_from" // 发件人地址 + SettingKeySmtpFromName = "smtp_from_name" // 发件人名称 + SettingKeySmtpUseTLS = "smtp_use_tls" // 是否使用TLS + + // Cloudflare Turnstile 设置 + SettingKeyTurnstileEnabled = "turnstile_enabled" // 是否启用 Turnstile 验证 + SettingKeyTurnstileSiteKey = "turnstile_site_key" // Turnstile Site Key + SettingKeyTurnstileSecretKey = "turnstile_secret_key" // Turnstile Secret Key + + // OEM设置 + SettingKeySiteName = "site_name" // 网站名称 + SettingKeySiteLogo = "site_logo" // 网站Logo (base64) + SettingKeySiteSubtitle = "site_subtitle" // 网站副标题 + SettingKeyApiBaseUrl = "api_base_url" // API端点地址(用于客户端配置和导入) + SettingKeyContactInfo = "contact_info" // 客服联系方式 + + // 默认配置 + SettingKeyDefaultConcurrency = "default_concurrency" // 新用户默认并发量 + SettingKeyDefaultBalance = "default_balance" // 新用户默认余额 +) + +// SystemSettings 系统设置结构体(用于API响应) +type SystemSettings struct { + // 注册设置 + RegistrationEnabled bool `json:"registration_enabled"` + EmailVerifyEnabled bool `json:"email_verify_enabled"` + + // 邮件服务设置 + SmtpHost string `json:"smtp_host"` + SmtpPort int `json:"smtp_port"` + SmtpUsername string `json:"smtp_username"` + SmtpPassword string `json:"smtp_password,omitempty"` // 不返回明文密码 + SmtpFrom string `json:"smtp_from_email"` + SmtpFromName string `json:"smtp_from_name"` + SmtpUseTLS bool `json:"smtp_use_tls"` + + // Cloudflare Turnstile 设置 + TurnstileEnabled bool `json:"turnstile_enabled"` + TurnstileSiteKey string `json:"turnstile_site_key"` + TurnstileSecretKey string `json:"turnstile_secret_key,omitempty"` // 不返回明文密钥 + + // OEM设置 + SiteName string `json:"site_name"` + SiteLogo string `json:"site_logo"` + SiteSubtitle string `json:"site_subtitle"` + ApiBaseUrl string `json:"api_base_url"` + ContactInfo string `json:"contact_info"` + + // 默认配置 + DefaultConcurrency int `json:"default_concurrency"` + DefaultBalance float64 `json:"default_balance"` +} + +// PublicSettings 公开设置(无需登录即可获取) +type PublicSettings struct { + RegistrationEnabled bool `json:"registration_enabled"` + EmailVerifyEnabled bool `json:"email_verify_enabled"` + TurnstileEnabled bool `json:"turnstile_enabled"` + TurnstileSiteKey string `json:"turnstile_site_key"` + SiteName string `json:"site_name"` + SiteLogo string `json:"site_logo"` + SiteSubtitle string `json:"site_subtitle"` + ApiBaseUrl string `json:"api_base_url"` + ContactInfo string `json:"contact_info"` + Version string `json:"version"` +} diff --git a/backend/internal/model/usage_log.go b/backend/internal/model/usage_log.go new file mode 100644 index 00000000..fb23cf73 --- /dev/null +++ b/backend/internal/model/usage_log.go @@ -0,0 +1,67 @@ +package model + +import ( + "time" +) + +// 消费类型常量 +const ( + BillingTypeBalance int8 = 0 // 钱包余额 + BillingTypeSubscription int8 = 1 // 订阅套餐 +) + +type UsageLog struct { + ID int64 `gorm:"primaryKey" json:"id"` + UserID int64 `gorm:"index;not null" json:"user_id"` + ApiKeyID int64 `gorm:"index;not null" json:"api_key_id"` + AccountID int64 `gorm:"index;not null" json:"account_id"` + RequestID string `gorm:"size:64" json:"request_id"` + Model string `gorm:"size:100;index;not null" json:"model"` + + // 订阅关联(可选) + GroupID *int64 `gorm:"index" json:"group_id"` + SubscriptionID *int64 `gorm:"index" json:"subscription_id"` + + // Token使用量(4类) + InputTokens int `gorm:"default:0;not null" json:"input_tokens"` + OutputTokens int `gorm:"default:0;not null" json:"output_tokens"` + CacheCreationTokens int `gorm:"default:0;not null" json:"cache_creation_tokens"` + CacheReadTokens int `gorm:"default:0;not null" json:"cache_read_tokens"` + + // 详细的缓存创建分类 + CacheCreation5mTokens int `gorm:"default:0;not null" json:"cache_creation_5m_tokens"` + CacheCreation1hTokens int `gorm:"default:0;not null" json:"cache_creation_1h_tokens"` + + // 费用(USD) + InputCost float64 `gorm:"type:decimal(20,10);default:0;not null" json:"input_cost"` + OutputCost float64 `gorm:"type:decimal(20,10);default:0;not null" json:"output_cost"` + CacheCreationCost float64 `gorm:"type:decimal(20,10);default:0;not null" json:"cache_creation_cost"` + CacheReadCost float64 `gorm:"type:decimal(20,10);default:0;not null" json:"cache_read_cost"` + TotalCost float64 `gorm:"type:decimal(20,10);default:0;not null" json:"total_cost"` // 原始总费用 + ActualCost float64 `gorm:"type:decimal(20,10);default:0;not null" json:"actual_cost"` // 实际扣除费用 + RateMultiplier float64 `gorm:"type:decimal(10,4);default:1;not null" json:"rate_multiplier"` // 计费倍率 + + // 元数据 + BillingType int8 `gorm:"type:smallint;default:0;not null" json:"billing_type"` // 0=余额 1=订阅 + Stream bool `gorm:"default:false;not null" json:"stream"` + DurationMs *int `json:"duration_ms"` + FirstTokenMs *int `json:"first_token_ms"` // 首字时间(流式请求) + + CreatedAt time.Time `gorm:"index;not null" json:"created_at"` + + // 关联 + User *User `gorm:"foreignKey:UserID" json:"user,omitempty"` + ApiKey *ApiKey `gorm:"foreignKey:ApiKeyID" json:"api_key,omitempty"` + Account *Account `gorm:"foreignKey:AccountID" json:"account,omitempty"` + Group *Group `gorm:"foreignKey:GroupID" json:"group,omitempty"` + Subscription *UserSubscription `gorm:"foreignKey:SubscriptionID" json:"subscription,omitempty"` +} + +func (UsageLog) TableName() string { + return "usage_logs" +} + +// TotalTokens 总token数 +func (u *UsageLog) TotalTokens() int { + return u.InputTokens + u.OutputTokens + u.CacheCreationTokens + u.CacheReadTokens +} diff --git a/backend/internal/model/user.go b/backend/internal/model/user.go new file mode 100644 index 00000000..96552474 --- /dev/null +++ b/backend/internal/model/user.go @@ -0,0 +1,74 @@ +package model + +import ( + "time" + + "github.com/lib/pq" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +type User struct { + ID int64 `gorm:"primaryKey" json:"id"` + Email string `gorm:"uniqueIndex;size:255;not null" json:"email"` + PasswordHash string `gorm:"size:255;not null" json:"-"` + Role string `gorm:"size:20;default:user;not null" json:"role"` // admin/user + Balance float64 `gorm:"type:decimal(20,8);default:0;not null" json:"balance"` + Concurrency int `gorm:"default:5;not null" json:"concurrency"` + Status string `gorm:"size:20;default:active;not null" json:"status"` // active/disabled + AllowedGroups pq.Int64Array `gorm:"type:bigint[]" json:"allowed_groups"` + CreatedAt time.Time `gorm:"not null" json:"created_at"` + UpdatedAt time.Time `gorm:"not null" json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + // 关联 + ApiKeys []ApiKey `gorm:"foreignKey:UserID" json:"api_keys,omitempty"` +} + +func (User) TableName() string { + return "users" +} + +// IsAdmin 检查是否管理员 +func (u *User) IsAdmin() bool { + return u.Role == "admin" +} + +// IsActive 检查是否激活 +func (u *User) IsActive() bool { + return u.Status == "active" +} + +// CanBindGroup 检查是否可以绑定指定分组 +// 对于标准类型分组: +// - 如果 AllowedGroups 设置了值(非空数组),只能绑定列表中的分组 +// - 如果 AllowedGroups 为 nil 或空数组,可以绑定所有非专属分组 +func (u *User) CanBindGroup(groupID int64, isExclusive bool) bool { + // 如果设置了 allowed_groups 且不为空,只能绑定指定的分组 + if len(u.AllowedGroups) > 0 { + for _, id := range u.AllowedGroups { + if id == groupID { + return true + } + } + return false + } + // 如果没有设置 allowed_groups 或为空数组,可以绑定所有非专属分组 + return !isExclusive +} + +// SetPassword 设置密码(哈希存储) +func (u *User) SetPassword(password string) error { + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return err + } + u.PasswordHash = string(hash) + return nil +} + +// CheckPassword 验证密码 +func (u *User) CheckPassword(password string) bool { + err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)) + return err == nil +} diff --git a/backend/internal/model/user_subscription.go b/backend/internal/model/user_subscription.go new file mode 100644 index 00000000..2bdcd1b5 --- /dev/null +++ b/backend/internal/model/user_subscription.go @@ -0,0 +1,157 @@ +package model + +import ( + "time" +) + +// 订阅状态常量 +const ( + SubscriptionStatusActive = "active" + SubscriptionStatusExpired = "expired" + SubscriptionStatusSuspended = "suspended" +) + +// UserSubscription 用户订阅模型 +type UserSubscription struct { + ID int64 `gorm:"primaryKey" json:"id"` + UserID int64 `gorm:"index;not null" json:"user_id"` + GroupID int64 `gorm:"index;not null" json:"group_id"` + + // 订阅有效期 + StartsAt time.Time `gorm:"not null" json:"starts_at"` + ExpiresAt time.Time `gorm:"not null" json:"expires_at"` + Status string `gorm:"size:20;default:active;not null" json:"status"` // active/expired/suspended + + // 滑动窗口起始时间(nil = 未激活) + DailyWindowStart *time.Time `json:"daily_window_start"` + WeeklyWindowStart *time.Time `json:"weekly_window_start"` + MonthlyWindowStart *time.Time `json:"monthly_window_start"` + + // 当前窗口已用额度(USD,基于 total_cost 计算) + DailyUsageUSD float64 `gorm:"type:decimal(20,10);default:0;not null" json:"daily_usage_usd"` + WeeklyUsageUSD float64 `gorm:"type:decimal(20,10);default:0;not null" json:"weekly_usage_usd"` + MonthlyUsageUSD float64 `gorm:"type:decimal(20,10);default:0;not null" json:"monthly_usage_usd"` + + // 管理员分配信息 + AssignedBy *int64 `gorm:"index" json:"assigned_by"` + AssignedAt time.Time `gorm:"not null" json:"assigned_at"` + Notes string `gorm:"type:text" json:"notes"` + + CreatedAt time.Time `gorm:"not null" json:"created_at"` + UpdatedAt time.Time `gorm:"not null" json:"updated_at"` + + // 关联 + User *User `gorm:"foreignKey:UserID" json:"user,omitempty"` + Group *Group `gorm:"foreignKey:GroupID" json:"group,omitempty"` + AssignedByUser *User `gorm:"foreignKey:AssignedBy" json:"assigned_by_user,omitempty"` +} + +func (UserSubscription) TableName() string { + return "user_subscriptions" +} + +// IsActive 检查订阅是否有效(状态为active且未过期) +func (s *UserSubscription) IsActive() bool { + return s.Status == SubscriptionStatusActive && time.Now().Before(s.ExpiresAt) +} + +// IsExpired 检查订阅是否已过期 +func (s *UserSubscription) IsExpired() bool { + return time.Now().After(s.ExpiresAt) +} + +// DaysRemaining 返回订阅剩余天数 +func (s *UserSubscription) DaysRemaining() int { + if s.IsExpired() { + return 0 + } + return int(time.Until(s.ExpiresAt).Hours() / 24) +} + +// IsWindowActivated 检查窗口是否已激活 +func (s *UserSubscription) IsWindowActivated() bool { + return s.DailyWindowStart != nil || s.WeeklyWindowStart != nil || s.MonthlyWindowStart != nil +} + +// NeedsDailyReset 检查日窗口是否需要重置 +func (s *UserSubscription) NeedsDailyReset() bool { + if s.DailyWindowStart == nil { + return false + } + return time.Since(*s.DailyWindowStart) >= 24*time.Hour +} + +// NeedsWeeklyReset 检查周窗口是否需要重置 +func (s *UserSubscription) NeedsWeeklyReset() bool { + if s.WeeklyWindowStart == nil { + return false + } + return time.Since(*s.WeeklyWindowStart) >= 7*24*time.Hour +} + +// NeedsMonthlyReset 检查月窗口是否需要重置 +func (s *UserSubscription) NeedsMonthlyReset() bool { + if s.MonthlyWindowStart == nil { + return false + } + return time.Since(*s.MonthlyWindowStart) >= 30*24*time.Hour +} + +// DailyResetTime 返回日窗口重置时间 +func (s *UserSubscription) DailyResetTime() *time.Time { + if s.DailyWindowStart == nil { + return nil + } + t := s.DailyWindowStart.Add(24 * time.Hour) + return &t +} + +// WeeklyResetTime 返回周窗口重置时间 +func (s *UserSubscription) WeeklyResetTime() *time.Time { + if s.WeeklyWindowStart == nil { + return nil + } + t := s.WeeklyWindowStart.Add(7 * 24 * time.Hour) + return &t +} + +// MonthlyResetTime 返回月窗口重置时间 +func (s *UserSubscription) MonthlyResetTime() *time.Time { + if s.MonthlyWindowStart == nil { + return nil + } + t := s.MonthlyWindowStart.Add(30 * 24 * time.Hour) + return &t +} + +// CheckDailyLimit 检查是否超出日限额 +func (s *UserSubscription) CheckDailyLimit(group *Group, additionalCost float64) bool { + if !group.HasDailyLimit() { + return true // 无限制 + } + return s.DailyUsageUSD+additionalCost <= *group.DailyLimitUSD +} + +// CheckWeeklyLimit 检查是否超出周限额 +func (s *UserSubscription) CheckWeeklyLimit(group *Group, additionalCost float64) bool { + if !group.HasWeeklyLimit() { + return true // 无限制 + } + return s.WeeklyUsageUSD+additionalCost <= *group.WeeklyLimitUSD +} + +// CheckMonthlyLimit 检查是否超出月限额 +func (s *UserSubscription) CheckMonthlyLimit(group *Group, additionalCost float64) bool { + if !group.HasMonthlyLimit() { + return true // 无限制 + } + return s.MonthlyUsageUSD+additionalCost <= *group.MonthlyLimitUSD +} + +// CheckAllLimits 检查所有限额 +func (s *UserSubscription) CheckAllLimits(group *Group, additionalCost float64) (daily, weekly, monthly bool) { + daily = s.CheckDailyLimit(group, additionalCost) + weekly = s.CheckWeeklyLimit(group, additionalCost) + monthly = s.CheckMonthlyLimit(group, additionalCost) + return +} diff --git a/backend/internal/pkg/oauth/oauth.go b/backend/internal/pkg/oauth/oauth.go new file mode 100644 index 00000000..6e774e6d --- /dev/null +++ b/backend/internal/pkg/oauth/oauth.go @@ -0,0 +1,223 @@ +package oauth + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "fmt" + "net/url" + "strings" + "sync" + "time" +) + +// Claude OAuth Constants (from CRS project) +const ( + // OAuth Client ID for Claude + ClientID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e" + + // OAuth endpoints + AuthorizeURL = "https://claude.ai/oauth/authorize" + TokenURL = "https://console.anthropic.com/v1/oauth/token" + RedirectURI = "https://console.anthropic.com/oauth/code/callback" + + // Scopes + ScopeProfile = "user:profile" + ScopeInference = "user:inference" + + // Session TTL + SessionTTL = 30 * time.Minute +) + +// OAuthSession stores OAuth flow state +type OAuthSession struct { + State string `json:"state"` + CodeVerifier string `json:"code_verifier"` + Scope string `json:"scope"` + ProxyURL string `json:"proxy_url,omitempty"` + CreatedAt time.Time `json:"created_at"` +} + +// SessionStore manages OAuth sessions in memory +type SessionStore struct { + mu sync.RWMutex + sessions map[string]*OAuthSession +} + +// NewSessionStore creates a new session store +func NewSessionStore() *SessionStore { + store := &SessionStore{ + sessions: make(map[string]*OAuthSession), + } + // Start cleanup goroutine + go store.cleanup() + return store +} + +// Set stores a session +func (s *SessionStore) Set(sessionID string, session *OAuthSession) { + s.mu.Lock() + defer s.mu.Unlock() + s.sessions[sessionID] = session +} + +// Get retrieves a session +func (s *SessionStore) Get(sessionID string) (*OAuthSession, bool) { + s.mu.RLock() + defer s.mu.RUnlock() + session, ok := s.sessions[sessionID] + if !ok { + return nil, false + } + // Check if expired + if time.Since(session.CreatedAt) > SessionTTL { + return nil, false + } + return session, true +} + +// Delete removes a session +func (s *SessionStore) Delete(sessionID string) { + s.mu.Lock() + defer s.mu.Unlock() + delete(s.sessions, sessionID) +} + +// cleanup removes expired sessions periodically +func (s *SessionStore) cleanup() { + ticker := time.NewTicker(5 * time.Minute) + for range ticker.C { + s.mu.Lock() + for id, session := range s.sessions { + if time.Since(session.CreatedAt) > SessionTTL { + delete(s.sessions, id) + } + } + s.mu.Unlock() + } +} + +// GenerateRandomBytes generates cryptographically secure random bytes +func GenerateRandomBytes(n int) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + return nil, err + } + return b, nil +} + +// GenerateState generates a random state string for OAuth +func GenerateState() (string, error) { + bytes, err := GenerateRandomBytes(32) + if err != nil { + return "", err + } + return hex.EncodeToString(bytes), nil +} + +// GenerateSessionID generates a unique session ID +func GenerateSessionID() (string, error) { + bytes, err := GenerateRandomBytes(16) + if err != nil { + return "", err + } + return hex.EncodeToString(bytes), nil +} + +// GenerateCodeVerifier generates a PKCE code verifier (32 bytes -> base64url) +func GenerateCodeVerifier() (string, error) { + bytes, err := GenerateRandomBytes(32) + if err != nil { + return "", err + } + return base64URLEncode(bytes), nil +} + +// GenerateCodeChallenge generates a PKCE code challenge using S256 method +func GenerateCodeChallenge(verifier string) string { + hash := sha256.Sum256([]byte(verifier)) + return base64URLEncode(hash[:]) +} + +// base64URLEncode encodes bytes to base64url without padding +func base64URLEncode(data []byte) string { + encoded := base64.URLEncoding.EncodeToString(data) + // Remove padding + return strings.TrimRight(encoded, "=") +} + +// BuildAuthorizationURL builds the OAuth authorization URL +func BuildAuthorizationURL(state, codeChallenge, scope string) string { + params := url.Values{} + params.Set("response_type", "code") + params.Set("client_id", ClientID) + params.Set("redirect_uri", RedirectURI) + params.Set("scope", scope) + params.Set("state", state) + params.Set("code_challenge", codeChallenge) + params.Set("code_challenge_method", "S256") + + return fmt.Sprintf("%s?%s", AuthorizeURL, params.Encode()) +} + +// TokenRequest represents the token exchange request body +type TokenRequest struct { + GrantType string `json:"grant_type"` + ClientID string `json:"client_id"` + Code string `json:"code"` + RedirectURI string `json:"redirect_uri"` + CodeVerifier string `json:"code_verifier"` + State string `json:"state"` +} + +// TokenResponse represents the token response from OAuth provider +type TokenResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int64 `json:"expires_in"` + RefreshToken string `json:"refresh_token,omitempty"` + Scope string `json:"scope,omitempty"` + // Organization and Account info from OAuth response + Organization *OrgInfo `json:"organization,omitempty"` + Account *AccountInfo `json:"account,omitempty"` +} + +// OrgInfo represents organization info from OAuth response +type OrgInfo struct { + UUID string `json:"uuid"` +} + +// AccountInfo represents account info from OAuth response +type AccountInfo struct { + UUID string `json:"uuid"` +} + +// RefreshTokenRequest represents the refresh token request +type RefreshTokenRequest struct { + GrantType string `json:"grant_type"` + RefreshToken string `json:"refresh_token"` + ClientID string `json:"client_id"` +} + +// BuildTokenRequest creates a token exchange request +func BuildTokenRequest(code, codeVerifier, state string) *TokenRequest { + return &TokenRequest{ + GrantType: "authorization_code", + ClientID: ClientID, + Code: code, + RedirectURI: RedirectURI, + CodeVerifier: codeVerifier, + State: state, + } +} + +// BuildRefreshTokenRequest creates a refresh token request +func BuildRefreshTokenRequest(refreshToken string) *RefreshTokenRequest { + return &RefreshTokenRequest{ + GrantType: "refresh_token", + RefreshToken: refreshToken, + ClientID: ClientID, + } +} diff --git a/backend/internal/pkg/response/response.go b/backend/internal/pkg/response/response.go new file mode 100644 index 00000000..42acbd35 --- /dev/null +++ b/backend/internal/pkg/response/response.go @@ -0,0 +1,157 @@ +package response + +import ( + "math" + "net/http" + + "github.com/gin-gonic/gin" +) + +// Response 标准API响应格式 +type Response struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` +} + +// PaginatedData 分页数据格式(匹配前端期望) +type PaginatedData struct { + Items interface{} `json:"items"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + Pages int `json:"pages"` +} + +// Success 返回成功响应 +func Success(c *gin.Context, data interface{}) { + c.JSON(http.StatusOK, Response{ + Code: 0, + Message: "success", + Data: data, + }) +} + +// Created 返回创建成功响应 +func Created(c *gin.Context, data interface{}) { + c.JSON(http.StatusCreated, Response{ + Code: 0, + Message: "success", + Data: data, + }) +} + +// Error 返回错误响应 +func Error(c *gin.Context, statusCode int, message string) { + c.JSON(statusCode, Response{ + Code: statusCode, + Message: message, + }) +} + +// BadRequest 返回400错误 +func BadRequest(c *gin.Context, message string) { + Error(c, http.StatusBadRequest, message) +} + +// Unauthorized 返回401错误 +func Unauthorized(c *gin.Context, message string) { + Error(c, http.StatusUnauthorized, message) +} + +// Forbidden 返回403错误 +func Forbidden(c *gin.Context, message string) { + Error(c, http.StatusForbidden, message) +} + +// NotFound 返回404错误 +func NotFound(c *gin.Context, message string) { + Error(c, http.StatusNotFound, message) +} + +// InternalError 返回500错误 +func InternalError(c *gin.Context, message string) { + Error(c, http.StatusInternalServerError, message) +} + +// Paginated 返回分页数据 +func Paginated(c *gin.Context, items interface{}, total int64, page, pageSize int) { + pages := int(math.Ceil(float64(total) / float64(pageSize))) + if pages < 1 { + pages = 1 + } + + Success(c, PaginatedData{ + Items: items, + Total: total, + Page: page, + PageSize: pageSize, + Pages: pages, + }) +} + +// PaginationResult 分页结果(与repository.PaginationResult兼容) +type PaginationResult struct { + Total int64 + Page int + PageSize int + Pages int +} + +// PaginatedWithResult 使用PaginationResult返回分页数据 +func PaginatedWithResult(c *gin.Context, items interface{}, pagination *PaginationResult) { + if pagination == nil { + Success(c, PaginatedData{ + Items: items, + Total: 0, + Page: 1, + PageSize: 20, + Pages: 1, + }) + return + } + + Success(c, PaginatedData{ + Items: items, + Total: pagination.Total, + Page: pagination.Page, + PageSize: pagination.PageSize, + Pages: pagination.Pages, + }) +} + +// ParsePagination 解析分页参数 +func ParsePagination(c *gin.Context) (page, pageSize int) { + page = 1 + pageSize = 20 + + if p := c.Query("page"); p != "" { + if val, err := parseInt(p); err == nil && val > 0 { + page = val + } + } + + // 支持 page_size 和 limit 两种参数名 + if ps := c.Query("page_size"); ps != "" { + if val, err := parseInt(ps); err == nil && val > 0 && val <= 100 { + pageSize = val + } + } else if l := c.Query("limit"); l != "" { + if val, err := parseInt(l); err == nil && val > 0 && val <= 100 { + pageSize = val + } + } + + return page, pageSize +} + +func parseInt(s string) (int, error) { + var result int + for _, c := range s { + if c < '0' || c > '9' { + return 0, nil + } + result = result*10 + int(c-'0') + } + return result, nil +} diff --git a/backend/internal/pkg/timezone/timezone.go b/backend/internal/pkg/timezone/timezone.go new file mode 100644 index 00000000..35795648 --- /dev/null +++ b/backend/internal/pkg/timezone/timezone.go @@ -0,0 +1,124 @@ +// Package timezone provides global timezone management for the application. +// Similar to PHP's date_default_timezone_set, this package allows setting +// a global timezone that affects all time.Now() calls. +package timezone + +import ( + "fmt" + "log" + "time" +) + +var ( + // location is the global timezone location + location *time.Location + // tzName stores the timezone name for logging/debugging + tzName string +) + +// Init initializes the global timezone setting. +// This should be called once at application startup. +// Example timezone values: "Asia/Shanghai", "America/New_York", "UTC" +func Init(tz string) error { + if tz == "" { + tz = "Asia/Shanghai" // Default timezone + } + + loc, err := time.LoadLocation(tz) + if err != nil { + return fmt.Errorf("invalid timezone %q: %w", tz, err) + } + + // Set the global Go time.Local to our timezone + // This affects time.Now() throughout the application + time.Local = loc + location = loc + tzName = tz + + log.Printf("Timezone initialized: %s (UTC offset: %s)", tz, getUTCOffset(loc)) + return nil +} + +// getUTCOffset returns the current UTC offset for a location +func getUTCOffset(loc *time.Location) string { + _, offset := time.Now().In(loc).Zone() + hours := offset / 3600 + minutes := (offset % 3600) / 60 + if minutes < 0 { + minutes = -minutes + } + sign := "+" + if hours < 0 { + sign = "-" + hours = -hours + } + return fmt.Sprintf("%s%02d:%02d", sign, hours, minutes) +} + +// Now returns the current time in the configured timezone. +// This is equivalent to time.Now() after Init() is called, +// but provided for explicit timezone-aware code. +func Now() time.Time { + if location == nil { + return time.Now() + } + return time.Now().In(location) +} + +// Location returns the configured timezone location. +func Location() *time.Location { + if location == nil { + return time.Local + } + return location +} + +// Name returns the configured timezone name. +func Name() string { + if tzName == "" { + return "Local" + } + return tzName +} + +// StartOfDay returns the start of the given day (00:00:00) in the configured timezone. +func StartOfDay(t time.Time) time.Time { + loc := Location() + t = t.In(loc) + return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc) +} + +// Today returns the start of today (00:00:00) in the configured timezone. +func Today() time.Time { + return StartOfDay(Now()) +} + +// EndOfDay returns the end of the given day (23:59:59.999999999) in the configured timezone. +func EndOfDay(t time.Time) time.Time { + loc := Location() + t = t.In(loc) + return time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 999999999, loc) +} + +// StartOfWeek returns the start of the week (Monday 00:00:00) for the given time. +func StartOfWeek(t time.Time) time.Time { + loc := Location() + t = t.In(loc) + weekday := int(t.Weekday()) + if weekday == 0 { + weekday = 7 // Sunday is day 7 + } + return time.Date(t.Year(), t.Month(), t.Day()-weekday+1, 0, 0, 0, 0, loc) +} + +// StartOfMonth returns the start of the month (1st day 00:00:00) for the given time. +func StartOfMonth(t time.Time) time.Time { + loc := Location() + t = t.In(loc) + return time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, loc) +} + +// ParseInLocation parses a time string in the configured timezone. +func ParseInLocation(layout, value string) (time.Time, error) { + return time.ParseInLocation(layout, value, Location()) +} diff --git a/backend/internal/pkg/timezone/timezone_test.go b/backend/internal/pkg/timezone/timezone_test.go new file mode 100644 index 00000000..3d21246e --- /dev/null +++ b/backend/internal/pkg/timezone/timezone_test.go @@ -0,0 +1,127 @@ +package timezone + +import ( + "testing" + "time" +) + +func TestInit(t *testing.T) { + // Test with valid timezone + err := Init("Asia/Shanghai") + if err != nil { + t.Fatalf("Init failed with valid timezone: %v", err) + } + + // Verify time.Local was set + if time.Local.String() != "Asia/Shanghai" { + t.Errorf("time.Local not set correctly, got %s", time.Local.String()) + } + + // Verify our location variable + if Location().String() != "Asia/Shanghai" { + t.Errorf("Location() not set correctly, got %s", Location().String()) + } + + // Test Name() + if Name() != "Asia/Shanghai" { + t.Errorf("Name() not set correctly, got %s", Name()) + } +} + +func TestInitInvalidTimezone(t *testing.T) { + err := Init("Invalid/Timezone") + if err == nil { + t.Error("Init should fail with invalid timezone") + } +} + +func TestTimeNowAffected(t *testing.T) { + // Reset to UTC first + Init("UTC") + utcNow := time.Now() + + // Switch to Shanghai (UTC+8) + Init("Asia/Shanghai") + shanghaiNow := time.Now() + + // The times should be the same instant, but different timezone representation + // Shanghai should be 8 hours ahead in display + _, utcOffset := utcNow.Zone() + _, shanghaiOffset := shanghaiNow.Zone() + + expectedDiff := 8 * 3600 // 8 hours in seconds + actualDiff := shanghaiOffset - utcOffset + + if actualDiff != expectedDiff { + t.Errorf("Timezone offset difference incorrect: expected %d, got %d", expectedDiff, actualDiff) + } +} + +func TestToday(t *testing.T) { + Init("Asia/Shanghai") + + today := Today() + now := Now() + + // Today should be at 00:00:00 + if today.Hour() != 0 || today.Minute() != 0 || today.Second() != 0 { + t.Errorf("Today() not at start of day: %v", today) + } + + // Today should be same date as now + if today.Year() != now.Year() || today.Month() != now.Month() || today.Day() != now.Day() { + t.Errorf("Today() date mismatch: today=%v, now=%v", today, now) + } +} + +func TestStartOfDay(t *testing.T) { + Init("Asia/Shanghai") + + // Create a time at 15:30:45 + testTime := time.Date(2024, 6, 15, 15, 30, 45, 123456789, Location()) + startOfDay := StartOfDay(testTime) + + expected := time.Date(2024, 6, 15, 0, 0, 0, 0, Location()) + if !startOfDay.Equal(expected) { + t.Errorf("StartOfDay incorrect: expected %v, got %v", expected, startOfDay) + } +} + +func TestTruncateVsStartOfDay(t *testing.T) { + // This test demonstrates why Truncate(24*time.Hour) can be problematic + // and why StartOfDay is more reliable for timezone-aware code + + Init("Asia/Shanghai") + + now := Now() + + // Truncate operates on UTC, not local time + truncated := now.Truncate(24 * time.Hour) + + // StartOfDay operates on local time + startOfDay := StartOfDay(now) + + // These will likely be different for non-UTC timezones + t.Logf("Now: %v", now) + t.Logf("Truncate(24h): %v", truncated) + t.Logf("StartOfDay: %v", startOfDay) + + // The truncated time may not be at local midnight + // StartOfDay is always at local midnight + if startOfDay.Hour() != 0 { + t.Errorf("StartOfDay should be at hour 0, got %d", startOfDay.Hour()) + } +} + +func TestDSTAwareness(t *testing.T) { + // Test with a timezone that has DST (America/New_York) + err := Init("America/New_York") + if err != nil { + t.Skipf("America/New_York timezone not available: %v", err) + } + + // Just verify it doesn't crash + _ = Today() + _ = Now() + _ = StartOfDay(Now()) +} diff --git a/backend/internal/repository/account_repo.go b/backend/internal/repository/account_repo.go new file mode 100644 index 00000000..7f0fd7f2 --- /dev/null +++ b/backend/internal/repository/account_repo.go @@ -0,0 +1,268 @@ +package repository + +import ( + "context" + "sub2api/internal/model" + "time" + + "gorm.io/gorm" +) + +type AccountRepository struct { + db *gorm.DB +} + +func NewAccountRepository(db *gorm.DB) *AccountRepository { + return &AccountRepository{db: db} +} + +func (r *AccountRepository) Create(ctx context.Context, account *model.Account) error { + return r.db.WithContext(ctx).Create(account).Error +} + +func (r *AccountRepository) GetByID(ctx context.Context, id int64) (*model.Account, error) { + var account model.Account + err := r.db.WithContext(ctx).Preload("Proxy").Preload("AccountGroups").First(&account, id).Error + if err != nil { + return nil, err + } + // 填充 GroupIDs 虚拟字段 + account.GroupIDs = make([]int64, 0, len(account.AccountGroups)) + for _, ag := range account.AccountGroups { + account.GroupIDs = append(account.GroupIDs, ag.GroupID) + } + return &account, nil +} + +func (r *AccountRepository) Update(ctx context.Context, account *model.Account) error { + return r.db.WithContext(ctx).Save(account).Error +} + +func (r *AccountRepository) Delete(ctx context.Context, id int64) error { + // 先删除账号与分组的绑定关系 + if err := r.db.WithContext(ctx).Where("account_id = ?", id).Delete(&model.AccountGroup{}).Error; err != nil { + return err + } + // 再删除账号 + return r.db.WithContext(ctx).Delete(&model.Account{}, id).Error +} + +func (r *AccountRepository) List(ctx context.Context, params PaginationParams) ([]model.Account, *PaginationResult, error) { + return r.ListWithFilters(ctx, params, "", "", "", "") +} + +// ListWithFilters lists accounts with optional filtering by platform, type, status, and search query +func (r *AccountRepository) ListWithFilters(ctx context.Context, params PaginationParams, platform, accountType, status, search string) ([]model.Account, *PaginationResult, error) { + var accounts []model.Account + var total int64 + + db := r.db.WithContext(ctx).Model(&model.Account{}) + + // Apply filters + if platform != "" { + db = db.Where("platform = ?", platform) + } + if accountType != "" { + db = db.Where("type = ?", accountType) + } + if status != "" { + db = db.Where("status = ?", status) + } + if search != "" { + searchPattern := "%" + search + "%" + db = db.Where("name ILIKE ?", searchPattern) + } + + if err := db.Count(&total).Error; err != nil { + return nil, nil, err + } + + if err := db.Preload("Proxy").Preload("AccountGroups").Offset(params.Offset()).Limit(params.Limit()).Order("id DESC").Find(&accounts).Error; err != nil { + return nil, nil, err + } + + // 填充每个 Account 的 GroupIDs 虚拟字段 + for i := range accounts { + accounts[i].GroupIDs = make([]int64, 0, len(accounts[i].AccountGroups)) + for _, ag := range accounts[i].AccountGroups { + accounts[i].GroupIDs = append(accounts[i].GroupIDs, ag.GroupID) + } + } + + pages := int(total) / params.Limit() + if int(total)%params.Limit() > 0 { + pages++ + } + + return accounts, &PaginationResult{ + Total: total, + Page: params.Page, + PageSize: params.Limit(), + Pages: pages, + }, nil +} + +func (r *AccountRepository) ListByGroup(ctx context.Context, groupID int64) ([]model.Account, error) { + var accounts []model.Account + err := r.db.WithContext(ctx). + Joins("JOIN account_groups ON account_groups.account_id = accounts.id"). + Where("account_groups.group_id = ? AND accounts.status = ?", groupID, model.StatusActive). + Preload("Proxy"). + Order("account_groups.priority ASC, accounts.priority ASC"). + Find(&accounts).Error + return accounts, err +} + +func (r *AccountRepository) ListActive(ctx context.Context) ([]model.Account, error) { + var accounts []model.Account + err := r.db.WithContext(ctx). + Where("status = ?", model.StatusActive). + Preload("Proxy"). + Order("priority ASC"). + Find(&accounts).Error + return accounts, err +} + +func (r *AccountRepository) UpdateLastUsed(ctx context.Context, id int64) error { + now := time.Now() + return r.db.WithContext(ctx).Model(&model.Account{}).Where("id = ?", id).Update("last_used_at", now).Error +} + +func (r *AccountRepository) SetError(ctx context.Context, id int64, errorMsg string) error { + return r.db.WithContext(ctx).Model(&model.Account{}).Where("id = ?", id). + Updates(map[string]interface{}{ + "status": model.StatusError, + "error_message": errorMsg, + }).Error +} + +func (r *AccountRepository) AddToGroup(ctx context.Context, accountID, groupID int64, priority int) error { + ag := &model.AccountGroup{ + AccountID: accountID, + GroupID: groupID, + Priority: priority, + } + return r.db.WithContext(ctx).Create(ag).Error +} + +func (r *AccountRepository) RemoveFromGroup(ctx context.Context, accountID, groupID int64) error { + return r.db.WithContext(ctx).Where("account_id = ? AND group_id = ?", accountID, groupID). + Delete(&model.AccountGroup{}).Error +} + +func (r *AccountRepository) GetGroups(ctx context.Context, accountID int64) ([]model.Group, error) { + var groups []model.Group + err := r.db.WithContext(ctx). + Joins("JOIN account_groups ON account_groups.group_id = groups.id"). + Where("account_groups.account_id = ?", accountID). + Find(&groups).Error + return groups, err +} + +func (r *AccountRepository) ListByPlatform(ctx context.Context, platform string) ([]model.Account, error) { + var accounts []model.Account + err := r.db.WithContext(ctx). + Where("platform = ? AND status = ?", platform, model.StatusActive). + Preload("Proxy"). + Order("priority ASC"). + Find(&accounts).Error + return accounts, err +} + +func (r *AccountRepository) BindGroups(ctx context.Context, accountID int64, groupIDs []int64) error { + // 删除现有绑定 + if err := r.db.WithContext(ctx).Where("account_id = ?", accountID).Delete(&model.AccountGroup{}).Error; err != nil { + return err + } + + // 添加新绑定 + if len(groupIDs) > 0 { + accountGroups := make([]model.AccountGroup, 0, len(groupIDs)) + for i, groupID := range groupIDs { + accountGroups = append(accountGroups, model.AccountGroup{ + AccountID: accountID, + GroupID: groupID, + Priority: i + 1, // 使用索引作为优先级 + }) + } + return r.db.WithContext(ctx).Create(&accountGroups).Error + } + + return nil +} + +// ListSchedulable 获取所有可调度的账号 +func (r *AccountRepository) ListSchedulable(ctx context.Context) ([]model.Account, error) { + var accounts []model.Account + now := time.Now() + err := r.db.WithContext(ctx). + Where("status = ? AND schedulable = ?", model.StatusActive, true). + Where("(overload_until IS NULL OR overload_until <= ?)", now). + Where("(rate_limit_reset_at IS NULL OR rate_limit_reset_at <= ?)", now). + Preload("Proxy"). + Order("priority ASC"). + Find(&accounts).Error + return accounts, err +} + +// ListSchedulableByGroupID 按组获取可调度的账号 +func (r *AccountRepository) ListSchedulableByGroupID(ctx context.Context, groupID int64) ([]model.Account, error) { + var accounts []model.Account + now := time.Now() + err := r.db.WithContext(ctx). + Joins("JOIN account_groups ON account_groups.account_id = accounts.id"). + Where("account_groups.group_id = ?", groupID). + Where("accounts.status = ? AND accounts.schedulable = ?", model.StatusActive, true). + Where("(accounts.overload_until IS NULL OR accounts.overload_until <= ?)", now). + Where("(accounts.rate_limit_reset_at IS NULL OR accounts.rate_limit_reset_at <= ?)", now). + Preload("Proxy"). + Order("account_groups.priority ASC, accounts.priority ASC"). + Find(&accounts).Error + return accounts, err +} + +// SetRateLimited 标记账号为限流状态(429) +func (r *AccountRepository) SetRateLimited(ctx context.Context, id int64, resetAt time.Time) error { + now := time.Now() + return r.db.WithContext(ctx).Model(&model.Account{}).Where("id = ?", id). + Updates(map[string]interface{}{ + "rate_limited_at": now, + "rate_limit_reset_at": resetAt, + }).Error +} + +// SetOverloaded 标记账号为过载状态(529) +func (r *AccountRepository) SetOverloaded(ctx context.Context, id int64, until time.Time) error { + return r.db.WithContext(ctx).Model(&model.Account{}).Where("id = ?", id). + Update("overload_until", until).Error +} + +// ClearRateLimit 清除账号的限流状态 +func (r *AccountRepository) ClearRateLimit(ctx context.Context, id int64) error { + return r.db.WithContext(ctx).Model(&model.Account{}).Where("id = ?", id). + Updates(map[string]interface{}{ + "rate_limited_at": nil, + "rate_limit_reset_at": nil, + "overload_until": nil, + }).Error +} + +// UpdateSessionWindow 更新账号的5小时时间窗口信息 +func (r *AccountRepository) UpdateSessionWindow(ctx context.Context, id int64, start, end *time.Time, status string) error { + updates := map[string]interface{}{ + "session_window_status": status, + } + if start != nil { + updates["session_window_start"] = start + } + if end != nil { + updates["session_window_end"] = end + } + return r.db.WithContext(ctx).Model(&model.Account{}).Where("id = ?", id).Updates(updates).Error +} + +// SetSchedulable 设置账号的调度开关 +func (r *AccountRepository) SetSchedulable(ctx context.Context, id int64, schedulable bool) error { + return r.db.WithContext(ctx).Model(&model.Account{}).Where("id = ?", id). + Update("schedulable", schedulable).Error +} diff --git a/backend/internal/repository/api_key_repo.go b/backend/internal/repository/api_key_repo.go new file mode 100644 index 00000000..66ed8cdb --- /dev/null +++ b/backend/internal/repository/api_key_repo.go @@ -0,0 +1,149 @@ +package repository + +import ( + "context" + "sub2api/internal/model" + + "gorm.io/gorm" +) + +type ApiKeyRepository struct { + db *gorm.DB +} + +func NewApiKeyRepository(db *gorm.DB) *ApiKeyRepository { + return &ApiKeyRepository{db: db} +} + +func (r *ApiKeyRepository) Create(ctx context.Context, key *model.ApiKey) error { + return r.db.WithContext(ctx).Create(key).Error +} + +func (r *ApiKeyRepository) GetByID(ctx context.Context, id int64) (*model.ApiKey, error) { + var key model.ApiKey + err := r.db.WithContext(ctx).Preload("User").Preload("Group").First(&key, id).Error + if err != nil { + return nil, err + } + return &key, nil +} + +func (r *ApiKeyRepository) GetByKey(ctx context.Context, key string) (*model.ApiKey, error) { + var apiKey model.ApiKey + err := r.db.WithContext(ctx).Preload("User").Preload("Group").Where("key = ?", key).First(&apiKey).Error + if err != nil { + return nil, err + } + return &apiKey, nil +} + +func (r *ApiKeyRepository) Update(ctx context.Context, key *model.ApiKey) error { + return r.db.WithContext(ctx).Model(key).Select("name", "group_id", "status", "updated_at").Updates(key).Error +} + +func (r *ApiKeyRepository) Delete(ctx context.Context, id int64) error { + return r.db.WithContext(ctx).Delete(&model.ApiKey{}, id).Error +} + +func (r *ApiKeyRepository) ListByUserID(ctx context.Context, userID int64, params PaginationParams) ([]model.ApiKey, *PaginationResult, error) { + var keys []model.ApiKey + var total int64 + + db := r.db.WithContext(ctx).Model(&model.ApiKey{}).Where("user_id = ?", userID) + + if err := db.Count(&total).Error; err != nil { + return nil, nil, err + } + + if err := db.Preload("Group").Offset(params.Offset()).Limit(params.Limit()).Order("id DESC").Find(&keys).Error; err != nil { + return nil, nil, err + } + + pages := int(total) / params.Limit() + if int(total)%params.Limit() > 0 { + pages++ + } + + return keys, &PaginationResult{ + Total: total, + Page: params.Page, + PageSize: params.Limit(), + Pages: pages, + }, nil +} + +func (r *ApiKeyRepository) CountByUserID(ctx context.Context, userID int64) (int64, error) { + var count int64 + err := r.db.WithContext(ctx).Model(&model.ApiKey{}).Where("user_id = ?", userID).Count(&count).Error + return count, err +} + +func (r *ApiKeyRepository) ExistsByKey(ctx context.Context, key string) (bool, error) { + var count int64 + err := r.db.WithContext(ctx).Model(&model.ApiKey{}).Where("key = ?", key).Count(&count).Error + return count > 0, err +} + +func (r *ApiKeyRepository) ListByGroupID(ctx context.Context, groupID int64, params PaginationParams) ([]model.ApiKey, *PaginationResult, error) { + var keys []model.ApiKey + var total int64 + + db := r.db.WithContext(ctx).Model(&model.ApiKey{}).Where("group_id = ?", groupID) + + if err := db.Count(&total).Error; err != nil { + return nil, nil, err + } + + if err := db.Preload("User").Offset(params.Offset()).Limit(params.Limit()).Order("id DESC").Find(&keys).Error; err != nil { + return nil, nil, err + } + + pages := int(total) / params.Limit() + if int(total)%params.Limit() > 0 { + pages++ + } + + return keys, &PaginationResult{ + Total: total, + Page: params.Page, + PageSize: params.Limit(), + Pages: pages, + }, nil +} + +// SearchApiKeys searches API keys by user ID and/or keyword (name) +func (r *ApiKeyRepository) SearchApiKeys(ctx context.Context, userID int64, keyword string, limit int) ([]model.ApiKey, error) { + var keys []model.ApiKey + + db := r.db.WithContext(ctx).Model(&model.ApiKey{}) + + if userID > 0 { + db = db.Where("user_id = ?", userID) + } + + if keyword != "" { + searchPattern := "%" + keyword + "%" + db = db.Where("name ILIKE ?", searchPattern) + } + + if err := db.Limit(limit).Order("id DESC").Find(&keys).Error; err != nil { + return nil, err + } + + return keys, nil +} + +// ClearGroupIDByGroupID 将指定分组的所有 API Key 的 group_id 设为 nil +func (r *ApiKeyRepository) ClearGroupIDByGroupID(ctx context.Context, groupID int64) (int64, error) { + result := r.db.WithContext(ctx).Model(&model.ApiKey{}). + Where("group_id = ?", groupID). + Update("group_id", nil) + return result.RowsAffected, result.Error +} + +// CountByGroupID 获取分组的 API Key 数量 +func (r *ApiKeyRepository) CountByGroupID(ctx context.Context, groupID int64) (int64, error) { + var count int64 + err := r.db.WithContext(ctx).Model(&model.ApiKey{}).Where("group_id = ?", groupID).Count(&count).Error + return count, err +} diff --git a/backend/internal/repository/group_repo.go b/backend/internal/repository/group_repo.go new file mode 100644 index 00000000..19e72a15 --- /dev/null +++ b/backend/internal/repository/group_repo.go @@ -0,0 +1,137 @@ +package repository + +import ( + "context" + "sub2api/internal/model" + + "gorm.io/gorm" +) + +type GroupRepository struct { + db *gorm.DB +} + +func NewGroupRepository(db *gorm.DB) *GroupRepository { + return &GroupRepository{db: db} +} + +func (r *GroupRepository) Create(ctx context.Context, group *model.Group) error { + return r.db.WithContext(ctx).Create(group).Error +} + +func (r *GroupRepository) GetByID(ctx context.Context, id int64) (*model.Group, error) { + var group model.Group + err := r.db.WithContext(ctx).First(&group, id).Error + if err != nil { + return nil, err + } + return &group, nil +} + +func (r *GroupRepository) Update(ctx context.Context, group *model.Group) error { + return r.db.WithContext(ctx).Save(group).Error +} + +func (r *GroupRepository) Delete(ctx context.Context, id int64) error { + return r.db.WithContext(ctx).Delete(&model.Group{}, id).Error +} + +func (r *GroupRepository) List(ctx context.Context, params PaginationParams) ([]model.Group, *PaginationResult, error) { + return r.ListWithFilters(ctx, params, "", "", nil) +} + +// ListWithFilters lists groups with optional filtering by platform, status, and is_exclusive +func (r *GroupRepository) ListWithFilters(ctx context.Context, params PaginationParams, platform, status string, isExclusive *bool) ([]model.Group, *PaginationResult, error) { + var groups []model.Group + var total int64 + + db := r.db.WithContext(ctx).Model(&model.Group{}) + + // Apply filters + if platform != "" { + db = db.Where("platform = ?", platform) + } + if status != "" { + db = db.Where("status = ?", status) + } + if isExclusive != nil { + db = db.Where("is_exclusive = ?", *isExclusive) + } + + if err := db.Count(&total).Error; err != nil { + return nil, nil, err + } + + if err := db.Offset(params.Offset()).Limit(params.Limit()).Order("id ASC").Find(&groups).Error; err != nil { + return nil, nil, err + } + + // 获取每个分组的账号数量 + for i := range groups { + count, _ := r.GetAccountCount(ctx, groups[i].ID) + groups[i].AccountCount = count + } + + pages := int(total) / params.Limit() + if int(total)%params.Limit() > 0 { + pages++ + } + + return groups, &PaginationResult{ + Total: total, + Page: params.Page, + PageSize: params.Limit(), + Pages: pages, + }, nil +} + +func (r *GroupRepository) ListActive(ctx context.Context) ([]model.Group, error) { + var groups []model.Group + err := r.db.WithContext(ctx).Where("status = ?", model.StatusActive).Order("id ASC").Find(&groups).Error + if err != nil { + return nil, err + } + // 获取每个分组的账号数量 + for i := range groups { + count, _ := r.GetAccountCount(ctx, groups[i].ID) + groups[i].AccountCount = count + } + return groups, nil +} + +func (r *GroupRepository) ListActiveByPlatform(ctx context.Context, platform string) ([]model.Group, error) { + var groups []model.Group + err := r.db.WithContext(ctx).Where("status = ? AND platform = ?", model.StatusActive, platform).Order("id ASC").Find(&groups).Error + if err != nil { + return nil, err + } + // 获取每个分组的账号数量 + for i := range groups { + count, _ := r.GetAccountCount(ctx, groups[i].ID) + groups[i].AccountCount = count + } + return groups, nil +} + +func (r *GroupRepository) ExistsByName(ctx context.Context, name string) (bool, error) { + var count int64 + err := r.db.WithContext(ctx).Model(&model.Group{}).Where("name = ?", name).Count(&count).Error + return count > 0, err +} + +func (r *GroupRepository) GetAccountCount(ctx context.Context, groupID int64) (int64, error) { + var count int64 + err := r.db.WithContext(ctx).Model(&model.AccountGroup{}).Where("group_id = ?", groupID).Count(&count).Error + return count, err +} + +// DeleteAccountGroupsByGroupID 删除分组与账号的关联关系 +func (r *GroupRepository) DeleteAccountGroupsByGroupID(ctx context.Context, groupID int64) (int64, error) { + result := r.db.WithContext(ctx).Where("group_id = ?", groupID).Delete(&model.AccountGroup{}) + return result.RowsAffected, result.Error +} + +// DB 返回底层数据库连接,用于事务处理 +func (r *GroupRepository) DB() *gorm.DB { + return r.db +} diff --git a/backend/internal/repository/proxy_repo.go b/backend/internal/repository/proxy_repo.go new file mode 100644 index 00000000..1cd22cc8 --- /dev/null +++ b/backend/internal/repository/proxy_repo.go @@ -0,0 +1,161 @@ +package repository + +import ( + "context" + "sub2api/internal/model" + + "gorm.io/gorm" +) + +type ProxyRepository struct { + db *gorm.DB +} + +func NewProxyRepository(db *gorm.DB) *ProxyRepository { + return &ProxyRepository{db: db} +} + +func (r *ProxyRepository) Create(ctx context.Context, proxy *model.Proxy) error { + return r.db.WithContext(ctx).Create(proxy).Error +} + +func (r *ProxyRepository) GetByID(ctx context.Context, id int64) (*model.Proxy, error) { + var proxy model.Proxy + err := r.db.WithContext(ctx).First(&proxy, id).Error + if err != nil { + return nil, err + } + return &proxy, nil +} + +func (r *ProxyRepository) Update(ctx context.Context, proxy *model.Proxy) error { + return r.db.WithContext(ctx).Save(proxy).Error +} + +func (r *ProxyRepository) Delete(ctx context.Context, id int64) error { + return r.db.WithContext(ctx).Delete(&model.Proxy{}, id).Error +} + +func (r *ProxyRepository) List(ctx context.Context, params PaginationParams) ([]model.Proxy, *PaginationResult, error) { + return r.ListWithFilters(ctx, params, "", "", "") +} + +// ListWithFilters lists proxies with optional filtering by protocol, status, and search query +func (r *ProxyRepository) ListWithFilters(ctx context.Context, params PaginationParams, protocol, status, search string) ([]model.Proxy, *PaginationResult, error) { + var proxies []model.Proxy + var total int64 + + db := r.db.WithContext(ctx).Model(&model.Proxy{}) + + // Apply filters + if protocol != "" { + db = db.Where("protocol = ?", protocol) + } + if status != "" { + db = db.Where("status = ?", status) + } + if search != "" { + searchPattern := "%" + search + "%" + db = db.Where("name ILIKE ?", searchPattern) + } + + if err := db.Count(&total).Error; err != nil { + return nil, nil, err + } + + if err := db.Offset(params.Offset()).Limit(params.Limit()).Order("id DESC").Find(&proxies).Error; err != nil { + return nil, nil, err + } + + pages := int(total) / params.Limit() + if int(total)%params.Limit() > 0 { + pages++ + } + + return proxies, &PaginationResult{ + Total: total, + Page: params.Page, + PageSize: params.Limit(), + Pages: pages, + }, nil +} + +func (r *ProxyRepository) ListActive(ctx context.Context) ([]model.Proxy, error) { + var proxies []model.Proxy + err := r.db.WithContext(ctx).Where("status = ?", model.StatusActive).Find(&proxies).Error + return proxies, err +} + +// ExistsByHostPortAuth checks if a proxy with the same host, port, username, and password exists +func (r *ProxyRepository) ExistsByHostPortAuth(ctx context.Context, host string, port int, username, password string) (bool, error) { + var count int64 + err := r.db.WithContext(ctx).Model(&model.Proxy{}). + Where("host = ? AND port = ? AND username = ? AND password = ?", host, port, username, password). + Count(&count).Error + if err != nil { + return false, err + } + return count > 0, nil +} + +// CountAccountsByProxyID returns the number of accounts using a specific proxy +func (r *ProxyRepository) CountAccountsByProxyID(ctx context.Context, proxyID int64) (int64, error) { + var count int64 + err := r.db.WithContext(ctx).Model(&model.Account{}). + Where("proxy_id = ?", proxyID). + Count(&count).Error + return count, err +} + +// GetAccountCountsForProxies returns a map of proxy ID to account count for all proxies +func (r *ProxyRepository) GetAccountCountsForProxies(ctx context.Context) (map[int64]int64, error) { + type result struct { + ProxyID int64 `gorm:"column:proxy_id"` + Count int64 `gorm:"column:count"` + } + var results []result + err := r.db.WithContext(ctx). + Model(&model.Account{}). + Select("proxy_id, COUNT(*) as count"). + Where("proxy_id IS NOT NULL"). + Group("proxy_id"). + Scan(&results).Error + if err != nil { + return nil, err + } + + counts := make(map[int64]int64) + for _, r := range results { + counts[r.ProxyID] = r.Count + } + return counts, nil +} + +// ListActiveWithAccountCount returns all active proxies with account count, sorted by creation time descending +func (r *ProxyRepository) ListActiveWithAccountCount(ctx context.Context) ([]model.ProxyWithAccountCount, error) { + var proxies []model.Proxy + err := r.db.WithContext(ctx). + Where("status = ?", model.StatusActive). + Order("created_at DESC"). + Find(&proxies).Error + if err != nil { + return nil, err + } + + // Get account counts + counts, err := r.GetAccountCountsForProxies(ctx) + if err != nil { + return nil, err + } + + // Build result with account counts + result := make([]model.ProxyWithAccountCount, len(proxies)) + for i, proxy := range proxies { + result[i] = model.ProxyWithAccountCount{ + Proxy: proxy, + AccountCount: counts[proxy.ID], + } + } + + return result, nil +} diff --git a/backend/internal/repository/redeem_code_repo.go b/backend/internal/repository/redeem_code_repo.go new file mode 100644 index 00000000..90a44ba2 --- /dev/null +++ b/backend/internal/repository/redeem_code_repo.go @@ -0,0 +1,133 @@ +package repository + +import ( + "context" + "sub2api/internal/model" + "time" + + "gorm.io/gorm" +) + +type RedeemCodeRepository struct { + db *gorm.DB +} + +func NewRedeemCodeRepository(db *gorm.DB) *RedeemCodeRepository { + return &RedeemCodeRepository{db: db} +} + +func (r *RedeemCodeRepository) Create(ctx context.Context, code *model.RedeemCode) error { + return r.db.WithContext(ctx).Create(code).Error +} + +func (r *RedeemCodeRepository) CreateBatch(ctx context.Context, codes []model.RedeemCode) error { + return r.db.WithContext(ctx).Create(&codes).Error +} + +func (r *RedeemCodeRepository) GetByID(ctx context.Context, id int64) (*model.RedeemCode, error) { + var code model.RedeemCode + err := r.db.WithContext(ctx).First(&code, id).Error + if err != nil { + return nil, err + } + return &code, nil +} + +func (r *RedeemCodeRepository) GetByCode(ctx context.Context, code string) (*model.RedeemCode, error) { + var redeemCode model.RedeemCode + err := r.db.WithContext(ctx).Where("code = ?", code).First(&redeemCode).Error + if err != nil { + return nil, err + } + return &redeemCode, nil +} + +func (r *RedeemCodeRepository) Delete(ctx context.Context, id int64) error { + return r.db.WithContext(ctx).Delete(&model.RedeemCode{}, id).Error +} + +func (r *RedeemCodeRepository) List(ctx context.Context, params PaginationParams) ([]model.RedeemCode, *PaginationResult, error) { + return r.ListWithFilters(ctx, params, "", "", "") +} + +// ListWithFilters lists redeem codes with optional filtering by type, status, and search query +func (r *RedeemCodeRepository) ListWithFilters(ctx context.Context, params PaginationParams, codeType, status, search string) ([]model.RedeemCode, *PaginationResult, error) { + var codes []model.RedeemCode + var total int64 + + db := r.db.WithContext(ctx).Model(&model.RedeemCode{}) + + // Apply filters + if codeType != "" { + db = db.Where("type = ?", codeType) + } + if status != "" { + db = db.Where("status = ?", status) + } + if search != "" { + searchPattern := "%" + search + "%" + db = db.Where("code ILIKE ?", searchPattern) + } + + if err := db.Count(&total).Error; err != nil { + return nil, nil, err + } + + if err := db.Preload("User").Preload("Group").Offset(params.Offset()).Limit(params.Limit()).Order("id DESC").Find(&codes).Error; err != nil { + return nil, nil, err + } + + pages := int(total) / params.Limit() + if int(total)%params.Limit() > 0 { + pages++ + } + + return codes, &PaginationResult{ + Total: total, + Page: params.Page, + PageSize: params.Limit(), + Pages: pages, + }, nil +} + +func (r *RedeemCodeRepository) Update(ctx context.Context, code *model.RedeemCode) error { + return r.db.WithContext(ctx).Save(code).Error +} + +func (r *RedeemCodeRepository) Use(ctx context.Context, id, userID int64) error { + now := time.Now() + result := r.db.WithContext(ctx).Model(&model.RedeemCode{}). + Where("id = ? AND status = ?", id, model.StatusUnused). + Updates(map[string]interface{}{ + "status": model.StatusUsed, + "used_by": userID, + "used_at": now, + }) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound // 兑换码不存在或已被使用 + } + return nil +} + +// ListByUser returns all redeem codes used by a specific user +func (r *RedeemCodeRepository) ListByUser(ctx context.Context, userID int64, limit int) ([]model.RedeemCode, error) { + var codes []model.RedeemCode + if limit <= 0 { + limit = 10 + } + + err := r.db.WithContext(ctx). + Preload("Group"). + Where("used_by = ?", userID). + Order("used_at DESC"). + Limit(limit). + Find(&codes).Error + + if err != nil { + return nil, err + } + return codes, nil +} diff --git a/backend/internal/repository/repository.go b/backend/internal/repository/repository.go new file mode 100644 index 00000000..0c91910e --- /dev/null +++ b/backend/internal/repository/repository.go @@ -0,0 +1,74 @@ +package repository + +import ( + "gorm.io/gorm" +) + +// Repositories 所有仓库的集合 +type Repositories struct { + User *UserRepository + ApiKey *ApiKeyRepository + Group *GroupRepository + Account *AccountRepository + Proxy *ProxyRepository + RedeemCode *RedeemCodeRepository + UsageLog *UsageLogRepository + Setting *SettingRepository + UserSubscription *UserSubscriptionRepository +} + +// NewRepositories 创建所有仓库 +func NewRepositories(db *gorm.DB) *Repositories { + return &Repositories{ + User: NewUserRepository(db), + ApiKey: NewApiKeyRepository(db), + Group: NewGroupRepository(db), + Account: NewAccountRepository(db), + Proxy: NewProxyRepository(db), + RedeemCode: NewRedeemCodeRepository(db), + UsageLog: NewUsageLogRepository(db), + Setting: NewSettingRepository(db), + UserSubscription: NewUserSubscriptionRepository(db), + } +} + +// PaginationParams 分页参数 +type PaginationParams struct { + Page int + PageSize int +} + +// PaginationResult 分页结果 +type PaginationResult struct { + Total int64 + Page int + PageSize int + Pages int +} + +// DefaultPagination 默认分页参数 +func DefaultPagination() PaginationParams { + return PaginationParams{ + Page: 1, + PageSize: 20, + } +} + +// Offset 计算偏移量 +func (p PaginationParams) Offset() int { + if p.Page < 1 { + p.Page = 1 + } + return (p.Page - 1) * p.PageSize +} + +// Limit 获取限制数 +func (p PaginationParams) Limit() int { + if p.PageSize < 1 { + return 20 + } + if p.PageSize > 100 { + return 100 + } + return p.PageSize +} diff --git a/backend/internal/repository/setting_repo.go b/backend/internal/repository/setting_repo.go new file mode 100644 index 00000000..3d28d73e --- /dev/null +++ b/backend/internal/repository/setting_repo.go @@ -0,0 +1,108 @@ +package repository + +import ( + "context" + "sub2api/internal/model" + "time" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +// SettingRepository 系统设置数据访问层 +type SettingRepository struct { + db *gorm.DB +} + +// NewSettingRepository 创建系统设置仓库实例 +func NewSettingRepository(db *gorm.DB) *SettingRepository { + return &SettingRepository{db: db} +} + +// Get 根据Key获取设置值 +func (r *SettingRepository) Get(ctx context.Context, key string) (*model.Setting, error) { + var setting model.Setting + err := r.db.WithContext(ctx).Where("key = ?", key).First(&setting).Error + if err != nil { + return nil, err + } + return &setting, nil +} + +// GetValue 获取设置值字符串 +func (r *SettingRepository) GetValue(ctx context.Context, key string) (string, error) { + setting, err := r.Get(ctx, key) + if err != nil { + return "", err + } + return setting.Value, nil +} + +// Set 设置值(存在则更新,不存在则创建) +func (r *SettingRepository) Set(ctx context.Context, key, value string) error { + setting := &model.Setting{ + Key: key, + Value: value, + UpdatedAt: time.Now(), + } + + return r.db.WithContext(ctx).Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "key"}}, + DoUpdates: clause.AssignmentColumns([]string{"value", "updated_at"}), + }).Create(setting).Error +} + +// GetMultiple 批量获取设置 +func (r *SettingRepository) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) { + var settings []model.Setting + err := r.db.WithContext(ctx).Where("key IN ?", keys).Find(&settings).Error + if err != nil { + return nil, err + } + + result := make(map[string]string) + for _, s := range settings { + result[s.Key] = s.Value + } + return result, nil +} + +// SetMultiple 批量设置值 +func (r *SettingRepository) SetMultiple(ctx context.Context, settings map[string]string) error { + return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + for key, value := range settings { + setting := &model.Setting{ + Key: key, + Value: value, + UpdatedAt: time.Now(), + } + if err := tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "key"}}, + DoUpdates: clause.AssignmentColumns([]string{"value", "updated_at"}), + }).Create(setting).Error; err != nil { + return err + } + } + return nil + }) +} + +// GetAll 获取所有设置 +func (r *SettingRepository) GetAll(ctx context.Context) (map[string]string, error) { + var settings []model.Setting + err := r.db.WithContext(ctx).Find(&settings).Error + if err != nil { + return nil, err + } + + result := make(map[string]string) + for _, s := range settings { + result[s.Key] = s.Value + } + return result, nil +} + +// Delete 删除设置 +func (r *SettingRepository) Delete(ctx context.Context, key string) error { + return r.db.WithContext(ctx).Where("key = ?", key).Delete(&model.Setting{}).Error +} diff --git a/backend/internal/repository/usage_log_repo.go b/backend/internal/repository/usage_log_repo.go new file mode 100644 index 00000000..e8eab78e --- /dev/null +++ b/backend/internal/repository/usage_log_repo.go @@ -0,0 +1,1006 @@ +package repository + +import ( + "context" + "sub2api/internal/model" + "sub2api/internal/pkg/timezone" + "time" + + "gorm.io/gorm" +) + +type UsageLogRepository struct { + db *gorm.DB +} + +func NewUsageLogRepository(db *gorm.DB) *UsageLogRepository { + return &UsageLogRepository{db: db} +} + +func (r *UsageLogRepository) Create(ctx context.Context, log *model.UsageLog) error { + return r.db.WithContext(ctx).Create(log).Error +} + +func (r *UsageLogRepository) GetByID(ctx context.Context, id int64) (*model.UsageLog, error) { + var log model.UsageLog + err := r.db.WithContext(ctx).First(&log, id).Error + if err != nil { + return nil, err + } + return &log, nil +} + +func (r *UsageLogRepository) ListByUser(ctx context.Context, userID int64, params PaginationParams) ([]model.UsageLog, *PaginationResult, error) { + var logs []model.UsageLog + var total int64 + + db := r.db.WithContext(ctx).Model(&model.UsageLog{}).Where("user_id = ?", userID) + + if err := db.Count(&total).Error; err != nil { + return nil, nil, err + } + + if err := db.Offset(params.Offset()).Limit(params.Limit()).Order("id DESC").Find(&logs).Error; err != nil { + return nil, nil, err + } + + pages := int(total) / params.Limit() + if int(total)%params.Limit() > 0 { + pages++ + } + + return logs, &PaginationResult{ + Total: total, + Page: params.Page, + PageSize: params.Limit(), + Pages: pages, + }, nil +} + +func (r *UsageLogRepository) ListByApiKey(ctx context.Context, apiKeyID int64, params PaginationParams) ([]model.UsageLog, *PaginationResult, error) { + var logs []model.UsageLog + var total int64 + + db := r.db.WithContext(ctx).Model(&model.UsageLog{}).Where("api_key_id = ?", apiKeyID) + + if err := db.Count(&total).Error; err != nil { + return nil, nil, err + } + + if err := db.Offset(params.Offset()).Limit(params.Limit()).Order("id DESC").Find(&logs).Error; err != nil { + return nil, nil, err + } + + pages := int(total) / params.Limit() + if int(total)%params.Limit() > 0 { + pages++ + } + + return logs, &PaginationResult{ + Total: total, + Page: params.Page, + PageSize: params.Limit(), + Pages: pages, + }, nil +} + +// UserStats 用户使用统计 +type UserStats struct { + TotalRequests int64 `json:"total_requests"` + TotalTokens int64 `json:"total_tokens"` + TotalCost float64 `json:"total_cost"` + InputTokens int64 `json:"input_tokens"` + OutputTokens int64 `json:"output_tokens"` + CacheReadTokens int64 `json:"cache_read_tokens"` +} + +func (r *UsageLogRepository) GetUserStats(ctx context.Context, userID int64, startTime, endTime time.Time) (*UserStats, error) { + var stats UserStats + err := r.db.WithContext(ctx).Model(&model.UsageLog{}). + Select(` + COUNT(*) as total_requests, + COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as total_tokens, + COALESCE(SUM(actual_cost), 0) as total_cost, + COALESCE(SUM(input_tokens), 0) as input_tokens, + COALESCE(SUM(output_tokens), 0) as output_tokens, + COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens + `). + Where("user_id = ? AND created_at >= ? AND created_at < ?", userID, startTime, endTime). + Scan(&stats).Error + return &stats, err +} + +// DashboardStats 仪表盘统计 +type DashboardStats struct { + // 用户统计 + TotalUsers int64 `json:"total_users"` + TodayNewUsers int64 `json:"today_new_users"` // 今日新增用户数 + ActiveUsers int64 `json:"active_users"` // 今日有请求的用户数 + + // API Key 统计 + TotalApiKeys int64 `json:"total_api_keys"` + ActiveApiKeys int64 `json:"active_api_keys"` // 状态为 active 的 API Key 数 + + // 账户统计 + TotalAccounts int64 `json:"total_accounts"` + NormalAccounts int64 `json:"normal_accounts"` // 正常账户数 (schedulable=true, status=active) + ErrorAccounts int64 `json:"error_accounts"` // 异常账户数 (status=error) + RateLimitAccounts int64 `json:"ratelimit_accounts"` // 限流账户数 + OverloadAccounts int64 `json:"overload_accounts"` // 过载账户数 + + // 累计 Token 使用统计 + TotalRequests int64 `json:"total_requests"` + TotalInputTokens int64 `json:"total_input_tokens"` + TotalOutputTokens int64 `json:"total_output_tokens"` + TotalCacheCreationTokens int64 `json:"total_cache_creation_tokens"` + TotalCacheReadTokens int64 `json:"total_cache_read_tokens"` + TotalTokens int64 `json:"total_tokens"` + TotalCost float64 `json:"total_cost"` // 累计标准计费 + TotalActualCost float64 `json:"total_actual_cost"` // 累计实际扣除 + + // 今日 Token 使用统计 + TodayRequests int64 `json:"today_requests"` + TodayInputTokens int64 `json:"today_input_tokens"` + TodayOutputTokens int64 `json:"today_output_tokens"` + TodayCacheCreationTokens int64 `json:"today_cache_creation_tokens"` + TodayCacheReadTokens int64 `json:"today_cache_read_tokens"` + TodayTokens int64 `json:"today_tokens"` + TodayCost float64 `json:"today_cost"` // 今日标准计费 + TodayActualCost float64 `json:"today_actual_cost"` // 今日实际扣除 + + // 系统运行统计 + AverageDurationMs float64 `json:"average_duration_ms"` // 平均响应时间 +} + +func (r *UsageLogRepository) GetDashboardStats(ctx context.Context) (*DashboardStats, error) { + var stats DashboardStats + today := timezone.Today() + + // 总用户数 + r.db.WithContext(ctx).Model(&model.User{}).Count(&stats.TotalUsers) + + // 今日新增用户数 + r.db.WithContext(ctx).Model(&model.User{}). + Where("created_at >= ?", today). + Count(&stats.TodayNewUsers) + + // 今日活跃用户数 (今日有请求的用户) + r.db.WithContext(ctx).Model(&model.UsageLog{}). + Distinct("user_id"). + Where("created_at >= ?", today). + Count(&stats.ActiveUsers) + + // 总 API Key 数 + r.db.WithContext(ctx).Model(&model.ApiKey{}).Count(&stats.TotalApiKeys) + + // 活跃 API Key 数 + r.db.WithContext(ctx).Model(&model.ApiKey{}). + Where("status = ?", model.StatusActive). + Count(&stats.ActiveApiKeys) + + // 总账户数 + r.db.WithContext(ctx).Model(&model.Account{}).Count(&stats.TotalAccounts) + + // 正常账户数 (schedulable=true, status=active) + r.db.WithContext(ctx).Model(&model.Account{}). + Where("status = ? AND schedulable = ?", model.StatusActive, true). + Count(&stats.NormalAccounts) + + // 异常账户数 (status=error) + r.db.WithContext(ctx).Model(&model.Account{}). + Where("status = ?", model.StatusError). + Count(&stats.ErrorAccounts) + + // 限流账户数 + r.db.WithContext(ctx).Model(&model.Account{}). + Where("rate_limited_at IS NOT NULL AND rate_limit_reset_at > ?", time.Now()). + Count(&stats.RateLimitAccounts) + + // 过载账户数 + r.db.WithContext(ctx).Model(&model.Account{}). + Where("overload_until IS NOT NULL AND overload_until > ?", time.Now()). + Count(&stats.OverloadAccounts) + + // 累计 Token 统计 + var totalStats struct { + TotalRequests int64 `gorm:"column:total_requests"` + TotalInputTokens int64 `gorm:"column:total_input_tokens"` + TotalOutputTokens int64 `gorm:"column:total_output_tokens"` + TotalCacheCreationTokens int64 `gorm:"column:total_cache_creation_tokens"` + TotalCacheReadTokens int64 `gorm:"column:total_cache_read_tokens"` + TotalCost float64 `gorm:"column:total_cost"` + TotalActualCost float64 `gorm:"column:total_actual_cost"` + AverageDurationMs float64 `gorm:"column:avg_duration_ms"` + } + r.db.WithContext(ctx).Model(&model.UsageLog{}). + Select(` + COUNT(*) as total_requests, + COALESCE(SUM(input_tokens), 0) as total_input_tokens, + COALESCE(SUM(output_tokens), 0) as total_output_tokens, + COALESCE(SUM(cache_creation_tokens), 0) as total_cache_creation_tokens, + COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens, + COALESCE(SUM(total_cost), 0) as total_cost, + COALESCE(SUM(actual_cost), 0) as total_actual_cost, + COALESCE(AVG(duration_ms), 0) as avg_duration_ms + `). + Scan(&totalStats) + + stats.TotalRequests = totalStats.TotalRequests + stats.TotalInputTokens = totalStats.TotalInputTokens + stats.TotalOutputTokens = totalStats.TotalOutputTokens + stats.TotalCacheCreationTokens = totalStats.TotalCacheCreationTokens + stats.TotalCacheReadTokens = totalStats.TotalCacheReadTokens + stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheCreationTokens + stats.TotalCacheReadTokens + stats.TotalCost = totalStats.TotalCost + stats.TotalActualCost = totalStats.TotalActualCost + stats.AverageDurationMs = totalStats.AverageDurationMs + + // 今日 Token 统计 + var todayStats struct { + TodayRequests int64 `gorm:"column:today_requests"` + TodayInputTokens int64 `gorm:"column:today_input_tokens"` + TodayOutputTokens int64 `gorm:"column:today_output_tokens"` + TodayCacheCreationTokens int64 `gorm:"column:today_cache_creation_tokens"` + TodayCacheReadTokens int64 `gorm:"column:today_cache_read_tokens"` + TodayCost float64 `gorm:"column:today_cost"` + TodayActualCost float64 `gorm:"column:today_actual_cost"` + } + r.db.WithContext(ctx).Model(&model.UsageLog{}). + Select(` + COUNT(*) as today_requests, + COALESCE(SUM(input_tokens), 0) as today_input_tokens, + COALESCE(SUM(output_tokens), 0) as today_output_tokens, + COALESCE(SUM(cache_creation_tokens), 0) as today_cache_creation_tokens, + COALESCE(SUM(cache_read_tokens), 0) as today_cache_read_tokens, + COALESCE(SUM(total_cost), 0) as today_cost, + COALESCE(SUM(actual_cost), 0) as today_actual_cost + `). + Where("created_at >= ?", today). + Scan(&todayStats) + + stats.TodayRequests = todayStats.TodayRequests + stats.TodayInputTokens = todayStats.TodayInputTokens + stats.TodayOutputTokens = todayStats.TodayOutputTokens + stats.TodayCacheCreationTokens = todayStats.TodayCacheCreationTokens + stats.TodayCacheReadTokens = todayStats.TodayCacheReadTokens + stats.TodayTokens = stats.TodayInputTokens + stats.TodayOutputTokens + stats.TodayCacheCreationTokens + stats.TodayCacheReadTokens + stats.TodayCost = todayStats.TodayCost + stats.TodayActualCost = todayStats.TodayActualCost + + return &stats, nil +} + +func (r *UsageLogRepository) ListByAccount(ctx context.Context, accountID int64, params PaginationParams) ([]model.UsageLog, *PaginationResult, error) { + var logs []model.UsageLog + var total int64 + + db := r.db.WithContext(ctx).Model(&model.UsageLog{}).Where("account_id = ?", accountID) + + if err := db.Count(&total).Error; err != nil { + return nil, nil, err + } + + if err := db.Offset(params.Offset()).Limit(params.Limit()).Order("id DESC").Find(&logs).Error; err != nil { + return nil, nil, err + } + + pages := int(total) / params.Limit() + if int(total)%params.Limit() > 0 { + pages++ + } + + return logs, &PaginationResult{ + Total: total, + Page: params.Page, + PageSize: params.Limit(), + Pages: pages, + }, nil +} + +func (r *UsageLogRepository) ListByUserAndTimeRange(ctx context.Context, userID int64, startTime, endTime time.Time) ([]model.UsageLog, *PaginationResult, error) { + var logs []model.UsageLog + err := r.db.WithContext(ctx). + Where("user_id = ? AND created_at >= ? AND created_at < ?", userID, startTime, endTime). + Order("id DESC"). + Find(&logs).Error + return logs, nil, err +} + +func (r *UsageLogRepository) ListByApiKeyAndTimeRange(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) ([]model.UsageLog, *PaginationResult, error) { + var logs []model.UsageLog + err := r.db.WithContext(ctx). + Where("api_key_id = ? AND created_at >= ? AND created_at < ?", apiKeyID, startTime, endTime). + Order("id DESC"). + Find(&logs).Error + return logs, nil, err +} + +func (r *UsageLogRepository) ListByAccountAndTimeRange(ctx context.Context, accountID int64, startTime, endTime time.Time) ([]model.UsageLog, *PaginationResult, error) { + var logs []model.UsageLog + err := r.db.WithContext(ctx). + Where("account_id = ? AND created_at >= ? AND created_at < ?", accountID, startTime, endTime). + Order("id DESC"). + Find(&logs).Error + return logs, nil, err +} + +func (r *UsageLogRepository) ListByModelAndTimeRange(ctx context.Context, modelName string, startTime, endTime time.Time) ([]model.UsageLog, *PaginationResult, error) { + var logs []model.UsageLog + err := r.db.WithContext(ctx). + Where("model = ? AND created_at >= ? AND created_at < ?", modelName, startTime, endTime). + Order("id DESC"). + Find(&logs).Error + return logs, nil, err +} + +func (r *UsageLogRepository) Delete(ctx context.Context, id int64) error { + return r.db.WithContext(ctx).Delete(&model.UsageLog{}, id).Error +} + +// AccountStats 账号使用统计 +type AccountStats struct { + Requests int64 `json:"requests"` + Tokens int64 `json:"tokens"` + Cost float64 `json:"cost"` +} + +// GetAccountTodayStats 获取账号今日统计 +func (r *UsageLogRepository) GetAccountTodayStats(ctx context.Context, accountID int64) (*AccountStats, error) { + today := timezone.Today() + + var stats struct { + Requests int64 `gorm:"column:requests"` + Tokens int64 `gorm:"column:tokens"` + Cost float64 `gorm:"column:cost"` + } + + err := r.db.WithContext(ctx).Model(&model.UsageLog{}). + Select(` + COUNT(*) as requests, + COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as tokens, + COALESCE(SUM(actual_cost), 0) as cost + `). + Where("account_id = ? AND created_at >= ?", accountID, today). + Scan(&stats).Error + + if err != nil { + return nil, err + } + + return &AccountStats{ + Requests: stats.Requests, + Tokens: stats.Tokens, + Cost: stats.Cost, + }, nil +} + +// GetAccountWindowStats 获取账号时间窗口内的统计 +func (r *UsageLogRepository) GetAccountWindowStats(ctx context.Context, accountID int64, startTime time.Time) (*AccountStats, error) { + var stats struct { + Requests int64 `gorm:"column:requests"` + Tokens int64 `gorm:"column:tokens"` + Cost float64 `gorm:"column:cost"` + } + + err := r.db.WithContext(ctx).Model(&model.UsageLog{}). + Select(` + COUNT(*) as requests, + COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as tokens, + COALESCE(SUM(actual_cost), 0) as cost + `). + Where("account_id = ? AND created_at >= ?", accountID, startTime). + Scan(&stats).Error + + if err != nil { + return nil, err + } + + return &AccountStats{ + Requests: stats.Requests, + Tokens: stats.Tokens, + Cost: stats.Cost, + }, nil +} + +// TrendDataPoint represents a single point in trend data +type TrendDataPoint struct { + Date string `json:"date"` + Requests int64 `json:"requests"` + InputTokens int64 `json:"input_tokens"` + OutputTokens int64 `json:"output_tokens"` + CacheTokens int64 `json:"cache_tokens"` + TotalTokens int64 `json:"total_tokens"` + Cost float64 `json:"cost"` // 标准计费 + ActualCost float64 `json:"actual_cost"` // 实际扣除 +} + +// ModelStat represents usage statistics for a single model +type ModelStat struct { + Model string `json:"model"` + Requests int64 `json:"requests"` + InputTokens int64 `json:"input_tokens"` + OutputTokens int64 `json:"output_tokens"` + TotalTokens int64 `json:"total_tokens"` + Cost float64 `json:"cost"` // 标准计费 + ActualCost float64 `json:"actual_cost"` // 实际扣除 +} + +// UserUsageTrendPoint represents user usage trend data point +type UserUsageTrendPoint struct { + Date string `json:"date"` + UserID int64 `json:"user_id"` + Email string `json:"email"` + Requests int64 `json:"requests"` + Tokens int64 `json:"tokens"` + Cost float64 `json:"cost"` // 标准计费 + ActualCost float64 `json:"actual_cost"` // 实际扣除 +} + +// GetUsageTrend returns usage trend data grouped by date +// granularity: "day" or "hour" +func (r *UsageLogRepository) GetUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string) ([]TrendDataPoint, error) { + var results []TrendDataPoint + + // Choose date format based on granularity + var dateFormat string + if granularity == "hour" { + dateFormat = "YYYY-MM-DD HH24:00" + } else { + dateFormat = "YYYY-MM-DD" + } + + err := r.db.WithContext(ctx).Model(&model.UsageLog{}). + Select(` + TO_CHAR(created_at, ?) as date, + COUNT(*) as requests, + COALESCE(SUM(input_tokens), 0) as input_tokens, + COALESCE(SUM(output_tokens), 0) as output_tokens, + COALESCE(SUM(cache_creation_tokens + cache_read_tokens), 0) as cache_tokens, + COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as total_tokens, + COALESCE(SUM(total_cost), 0) as cost, + COALESCE(SUM(actual_cost), 0) as actual_cost + `, dateFormat). + Where("created_at >= ? AND created_at < ?", startTime, endTime). + Group("date"). + Order("date ASC"). + Scan(&results).Error + + if err != nil { + return nil, err + } + + return results, nil +} + +// GetModelStats returns usage statistics grouped by model +func (r *UsageLogRepository) GetModelStats(ctx context.Context, startTime, endTime time.Time) ([]ModelStat, error) { + var results []ModelStat + + err := r.db.WithContext(ctx).Model(&model.UsageLog{}). + Select(` + model, + COUNT(*) as requests, + COALESCE(SUM(input_tokens), 0) as input_tokens, + COALESCE(SUM(output_tokens), 0) as output_tokens, + COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as total_tokens, + COALESCE(SUM(total_cost), 0) as cost, + COALESCE(SUM(actual_cost), 0) as actual_cost + `). + Where("created_at >= ? AND created_at < ?", startTime, endTime). + Group("model"). + Order("total_tokens DESC"). + Scan(&results).Error + + if err != nil { + return nil, err + } + + return results, nil +} + +// ApiKeyUsageTrendPoint represents API key usage trend data point +type ApiKeyUsageTrendPoint struct { + Date string `json:"date"` + ApiKeyID int64 `json:"api_key_id"` + KeyName string `json:"key_name"` + Requests int64 `json:"requests"` + Tokens int64 `json:"tokens"` +} + +// GetApiKeyUsageTrend returns usage trend data grouped by API key and date +func (r *UsageLogRepository) GetApiKeyUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]ApiKeyUsageTrendPoint, error) { + var results []ApiKeyUsageTrendPoint + + // Choose date format based on granularity + var dateFormat string + if granularity == "hour" { + dateFormat = "YYYY-MM-DD HH24:00" + } else { + dateFormat = "YYYY-MM-DD" + } + + // Use raw SQL for complex subquery + query := ` + WITH top_keys AS ( + SELECT api_key_id + FROM usage_logs + WHERE created_at >= ? AND created_at < ? + GROUP BY api_key_id + ORDER BY SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens) DESC + LIMIT ? + ) + SELECT + TO_CHAR(u.created_at, '` + dateFormat + `') as date, + u.api_key_id, + COALESCE(k.name, '') as key_name, + COUNT(*) as requests, + COALESCE(SUM(u.input_tokens + u.output_tokens + u.cache_creation_tokens + u.cache_read_tokens), 0) as tokens + FROM usage_logs u + LEFT JOIN api_keys k ON u.api_key_id = k.id + WHERE u.api_key_id IN (SELECT api_key_id FROM top_keys) + AND u.created_at >= ? AND u.created_at < ? + GROUP BY date, u.api_key_id, k.name + ORDER BY date ASC, tokens DESC + ` + + err := r.db.WithContext(ctx).Raw(query, startTime, endTime, limit, startTime, endTime).Scan(&results).Error + if err != nil { + return nil, err + } + + return results, nil +} + +// GetUserUsageTrend returns usage trend data grouped by user and date +func (r *UsageLogRepository) GetUserUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]UserUsageTrendPoint, error) { + var results []UserUsageTrendPoint + + // Choose date format based on granularity + var dateFormat string + if granularity == "hour" { + dateFormat = "YYYY-MM-DD HH24:00" + } else { + dateFormat = "YYYY-MM-DD" + } + + // Use raw SQL for complex subquery + query := ` + WITH top_users AS ( + SELECT user_id + FROM usage_logs + WHERE created_at >= ? AND created_at < ? + GROUP BY user_id + ORDER BY SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens) DESC + LIMIT ? + ) + SELECT + TO_CHAR(u.created_at, '` + dateFormat + `') as date, + u.user_id, + COALESCE(us.email, '') as email, + COUNT(*) as requests, + COALESCE(SUM(u.input_tokens + u.output_tokens + u.cache_creation_tokens + u.cache_read_tokens), 0) as tokens, + COALESCE(SUM(u.total_cost), 0) as cost, + COALESCE(SUM(u.actual_cost), 0) as actual_cost + FROM usage_logs u + LEFT JOIN users us ON u.user_id = us.id + WHERE u.user_id IN (SELECT user_id FROM top_users) + AND u.created_at >= ? AND u.created_at < ? + GROUP BY date, u.user_id, us.email + ORDER BY date ASC, tokens DESC + ` + + err := r.db.WithContext(ctx).Raw(query, startTime, endTime, limit, startTime, endTime).Scan(&results).Error + if err != nil { + return nil, err + } + + return results, nil +} + +// UserDashboardStats 用户仪表盘统计 +type UserDashboardStats struct { + // API Key 统计 + TotalApiKeys int64 `json:"total_api_keys"` + ActiveApiKeys int64 `json:"active_api_keys"` + + // 累计 Token 使用统计 + TotalRequests int64 `json:"total_requests"` + TotalInputTokens int64 `json:"total_input_tokens"` + TotalOutputTokens int64 `json:"total_output_tokens"` + TotalCacheCreationTokens int64 `json:"total_cache_creation_tokens"` + TotalCacheReadTokens int64 `json:"total_cache_read_tokens"` + TotalTokens int64 `json:"total_tokens"` + TotalCost float64 `json:"total_cost"` // 累计标准计费 + TotalActualCost float64 `json:"total_actual_cost"` // 累计实际扣除 + + // 今日 Token 使用统计 + TodayRequests int64 `json:"today_requests"` + TodayInputTokens int64 `json:"today_input_tokens"` + TodayOutputTokens int64 `json:"today_output_tokens"` + TodayCacheCreationTokens int64 `json:"today_cache_creation_tokens"` + TodayCacheReadTokens int64 `json:"today_cache_read_tokens"` + TodayTokens int64 `json:"today_tokens"` + TodayCost float64 `json:"today_cost"` // 今日标准计费 + TodayActualCost float64 `json:"today_actual_cost"` // 今日实际扣除 + + // 性能统计 + AverageDurationMs float64 `json:"average_duration_ms"` +} + +// GetUserDashboardStats 获取用户专属的仪表盘统计 +func (r *UsageLogRepository) GetUserDashboardStats(ctx context.Context, userID int64) (*UserDashboardStats, error) { + var stats UserDashboardStats + today := timezone.Today() + + // API Key 统计 + r.db.WithContext(ctx).Model(&model.ApiKey{}). + Where("user_id = ?", userID). + Count(&stats.TotalApiKeys) + + r.db.WithContext(ctx).Model(&model.ApiKey{}). + Where("user_id = ? AND status = ?", userID, model.StatusActive). + Count(&stats.ActiveApiKeys) + + // 累计 Token 统计 + var totalStats struct { + TotalRequests int64 `gorm:"column:total_requests"` + TotalInputTokens int64 `gorm:"column:total_input_tokens"` + TotalOutputTokens int64 `gorm:"column:total_output_tokens"` + TotalCacheCreationTokens int64 `gorm:"column:total_cache_creation_tokens"` + TotalCacheReadTokens int64 `gorm:"column:total_cache_read_tokens"` + TotalCost float64 `gorm:"column:total_cost"` + TotalActualCost float64 `gorm:"column:total_actual_cost"` + AverageDurationMs float64 `gorm:"column:avg_duration_ms"` + } + r.db.WithContext(ctx).Model(&model.UsageLog{}). + Select(` + COUNT(*) as total_requests, + COALESCE(SUM(input_tokens), 0) as total_input_tokens, + COALESCE(SUM(output_tokens), 0) as total_output_tokens, + COALESCE(SUM(cache_creation_tokens), 0) as total_cache_creation_tokens, + COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens, + COALESCE(SUM(total_cost), 0) as total_cost, + COALESCE(SUM(actual_cost), 0) as total_actual_cost, + COALESCE(AVG(duration_ms), 0) as avg_duration_ms + `). + Where("user_id = ?", userID). + Scan(&totalStats) + + stats.TotalRequests = totalStats.TotalRequests + stats.TotalInputTokens = totalStats.TotalInputTokens + stats.TotalOutputTokens = totalStats.TotalOutputTokens + stats.TotalCacheCreationTokens = totalStats.TotalCacheCreationTokens + stats.TotalCacheReadTokens = totalStats.TotalCacheReadTokens + stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheCreationTokens + stats.TotalCacheReadTokens + stats.TotalCost = totalStats.TotalCost + stats.TotalActualCost = totalStats.TotalActualCost + stats.AverageDurationMs = totalStats.AverageDurationMs + + // 今日 Token 统计 + var todayStats struct { + TodayRequests int64 `gorm:"column:today_requests"` + TodayInputTokens int64 `gorm:"column:today_input_tokens"` + TodayOutputTokens int64 `gorm:"column:today_output_tokens"` + TodayCacheCreationTokens int64 `gorm:"column:today_cache_creation_tokens"` + TodayCacheReadTokens int64 `gorm:"column:today_cache_read_tokens"` + TodayCost float64 `gorm:"column:today_cost"` + TodayActualCost float64 `gorm:"column:today_actual_cost"` + } + r.db.WithContext(ctx).Model(&model.UsageLog{}). + Select(` + COUNT(*) as today_requests, + COALESCE(SUM(input_tokens), 0) as today_input_tokens, + COALESCE(SUM(output_tokens), 0) as today_output_tokens, + COALESCE(SUM(cache_creation_tokens), 0) as today_cache_creation_tokens, + COALESCE(SUM(cache_read_tokens), 0) as today_cache_read_tokens, + COALESCE(SUM(total_cost), 0) as today_cost, + COALESCE(SUM(actual_cost), 0) as today_actual_cost + `). + Where("user_id = ? AND created_at >= ?", userID, today). + Scan(&todayStats) + + stats.TodayRequests = todayStats.TodayRequests + stats.TodayInputTokens = todayStats.TodayInputTokens + stats.TodayOutputTokens = todayStats.TodayOutputTokens + stats.TodayCacheCreationTokens = todayStats.TodayCacheCreationTokens + stats.TodayCacheReadTokens = todayStats.TodayCacheReadTokens + stats.TodayTokens = stats.TodayInputTokens + stats.TodayOutputTokens + stats.TodayCacheCreationTokens + stats.TodayCacheReadTokens + stats.TodayCost = todayStats.TodayCost + stats.TodayActualCost = todayStats.TodayActualCost + + return &stats, nil +} + +// GetUserUsageTrendByUserID 获取指定用户的使用趋势 +func (r *UsageLogRepository) GetUserUsageTrendByUserID(ctx context.Context, userID int64, startTime, endTime time.Time, granularity string) ([]TrendDataPoint, error) { + var results []TrendDataPoint + + var dateFormat string + if granularity == "hour" { + dateFormat = "YYYY-MM-DD HH24:00" + } else { + dateFormat = "YYYY-MM-DD" + } + + err := r.db.WithContext(ctx).Model(&model.UsageLog{}). + Select(` + TO_CHAR(created_at, ?) as date, + COUNT(*) as requests, + COALESCE(SUM(input_tokens), 0) as input_tokens, + COALESCE(SUM(output_tokens), 0) as output_tokens, + COALESCE(SUM(cache_creation_tokens + cache_read_tokens), 0) as cache_tokens, + COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as total_tokens, + COALESCE(SUM(total_cost), 0) as cost, + COALESCE(SUM(actual_cost), 0) as actual_cost + `, dateFormat). + Where("user_id = ? AND created_at >= ? AND created_at < ?", userID, startTime, endTime). + Group("date"). + Order("date ASC"). + Scan(&results).Error + + if err != nil { + return nil, err + } + + return results, nil +} + +// GetUserModelStats 获取指定用户的模型统计 +func (r *UsageLogRepository) GetUserModelStats(ctx context.Context, userID int64, startTime, endTime time.Time) ([]ModelStat, error) { + var results []ModelStat + + err := r.db.WithContext(ctx).Model(&model.UsageLog{}). + Select(` + model, + COUNT(*) as requests, + COALESCE(SUM(input_tokens), 0) as input_tokens, + COALESCE(SUM(output_tokens), 0) as output_tokens, + COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as total_tokens, + COALESCE(SUM(total_cost), 0) as cost, + COALESCE(SUM(actual_cost), 0) as actual_cost + `). + Where("user_id = ? AND created_at >= ? AND created_at < ?", userID, startTime, endTime). + Group("model"). + Order("total_tokens DESC"). + Scan(&results).Error + + if err != nil { + return nil, err + } + + return results, nil +} + +// UsageLogFilters represents filters for usage log queries +type UsageLogFilters struct { + UserID int64 + ApiKeyID int64 + StartTime *time.Time + EndTime *time.Time +} + +// ListWithFilters lists usage logs with optional filters (for admin) +func (r *UsageLogRepository) ListWithFilters(ctx context.Context, params PaginationParams, filters UsageLogFilters) ([]model.UsageLog, *PaginationResult, error) { + var logs []model.UsageLog + var total int64 + + db := r.db.WithContext(ctx).Model(&model.UsageLog{}) + + // Apply filters + if filters.UserID > 0 { + db = db.Where("user_id = ?", filters.UserID) + } + if filters.ApiKeyID > 0 { + db = db.Where("api_key_id = ?", filters.ApiKeyID) + } + if filters.StartTime != nil { + db = db.Where("created_at >= ?", *filters.StartTime) + } + if filters.EndTime != nil { + db = db.Where("created_at <= ?", *filters.EndTime) + } + + if err := db.Count(&total).Error; err != nil { + return nil, nil, err + } + + // Preload user and api_key for display + if err := db.Preload("User").Preload("ApiKey"). + Offset(params.Offset()).Limit(params.Limit()). + Order("id DESC").Find(&logs).Error; err != nil { + return nil, nil, err + } + + pages := int(total) / params.Limit() + if int(total)%params.Limit() > 0 { + pages++ + } + + return logs, &PaginationResult{ + Total: total, + Page: params.Page, + PageSize: params.Limit(), + Pages: pages, + }, nil +} + +// UsageStats represents usage statistics +type UsageStats struct { + TotalRequests int64 `json:"total_requests"` + TotalInputTokens int64 `json:"total_input_tokens"` + TotalOutputTokens int64 `json:"total_output_tokens"` + TotalCacheTokens int64 `json:"total_cache_tokens"` + TotalTokens int64 `json:"total_tokens"` + TotalCost float64 `json:"total_cost"` + TotalActualCost float64 `json:"total_actual_cost"` + AverageDurationMs float64 `json:"average_duration_ms"` +} + +// BatchUserUsageStats represents usage stats for a single user +type BatchUserUsageStats struct { + UserID int64 `json:"user_id"` + TodayActualCost float64 `json:"today_actual_cost"` + TotalActualCost float64 `json:"total_actual_cost"` +} + +// GetBatchUserUsageStats gets today and total actual_cost for multiple users +func (r *UsageLogRepository) GetBatchUserUsageStats(ctx context.Context, userIDs []int64) (map[int64]*BatchUserUsageStats, error) { + if len(userIDs) == 0 { + return make(map[int64]*BatchUserUsageStats), nil + } + + today := timezone.Today() + result := make(map[int64]*BatchUserUsageStats) + + // Initialize result map + for _, id := range userIDs { + result[id] = &BatchUserUsageStats{UserID: id} + } + + // Get total actual_cost per user + var totalStats []struct { + UserID int64 `gorm:"column:user_id"` + TotalCost float64 `gorm:"column:total_cost"` + } + err := r.db.WithContext(ctx).Model(&model.UsageLog{}). + Select("user_id, COALESCE(SUM(actual_cost), 0) as total_cost"). + Where("user_id IN ?", userIDs). + Group("user_id"). + Scan(&totalStats).Error + if err != nil { + return nil, err + } + + for _, stat := range totalStats { + if s, ok := result[stat.UserID]; ok { + s.TotalActualCost = stat.TotalCost + } + } + + // Get today actual_cost per user + var todayStats []struct { + UserID int64 `gorm:"column:user_id"` + TodayCost float64 `gorm:"column:today_cost"` + } + err = r.db.WithContext(ctx).Model(&model.UsageLog{}). + Select("user_id, COALESCE(SUM(actual_cost), 0) as today_cost"). + Where("user_id IN ? AND created_at >= ?", userIDs, today). + Group("user_id"). + Scan(&todayStats).Error + if err != nil { + return nil, err + } + + for _, stat := range todayStats { + if s, ok := result[stat.UserID]; ok { + s.TodayActualCost = stat.TodayCost + } + } + + return result, nil +} + +// BatchApiKeyUsageStats represents usage stats for a single API key +type BatchApiKeyUsageStats struct { + ApiKeyID int64 `json:"api_key_id"` + TodayActualCost float64 `json:"today_actual_cost"` + TotalActualCost float64 `json:"total_actual_cost"` +} + +// GetBatchApiKeyUsageStats gets today and total actual_cost for multiple API keys +func (r *UsageLogRepository) GetBatchApiKeyUsageStats(ctx context.Context, apiKeyIDs []int64) (map[int64]*BatchApiKeyUsageStats, error) { + if len(apiKeyIDs) == 0 { + return make(map[int64]*BatchApiKeyUsageStats), nil + } + + today := timezone.Today() + result := make(map[int64]*BatchApiKeyUsageStats) + + // Initialize result map + for _, id := range apiKeyIDs { + result[id] = &BatchApiKeyUsageStats{ApiKeyID: id} + } + + // Get total actual_cost per api key + var totalStats []struct { + ApiKeyID int64 `gorm:"column:api_key_id"` + TotalCost float64 `gorm:"column:total_cost"` + } + err := r.db.WithContext(ctx).Model(&model.UsageLog{}). + Select("api_key_id, COALESCE(SUM(actual_cost), 0) as total_cost"). + Where("api_key_id IN ?", apiKeyIDs). + Group("api_key_id"). + Scan(&totalStats).Error + if err != nil { + return nil, err + } + + for _, stat := range totalStats { + if s, ok := result[stat.ApiKeyID]; ok { + s.TotalActualCost = stat.TotalCost + } + } + + // Get today actual_cost per api key + var todayStats []struct { + ApiKeyID int64 `gorm:"column:api_key_id"` + TodayCost float64 `gorm:"column:today_cost"` + } + err = r.db.WithContext(ctx).Model(&model.UsageLog{}). + Select("api_key_id, COALESCE(SUM(actual_cost), 0) as today_cost"). + Where("api_key_id IN ? AND created_at >= ?", apiKeyIDs, today). + Group("api_key_id"). + Scan(&todayStats).Error + if err != nil { + return nil, err + } + + for _, stat := range todayStats { + if s, ok := result[stat.ApiKeyID]; ok { + s.TodayActualCost = stat.TodayCost + } + } + + return result, nil +} + +// GetGlobalStats gets usage statistics for all users within a time range +func (r *UsageLogRepository) GetGlobalStats(ctx context.Context, startTime, endTime time.Time) (*UsageStats, error) { + var stats struct { + TotalRequests int64 `gorm:"column:total_requests"` + TotalInputTokens int64 `gorm:"column:total_input_tokens"` + TotalOutputTokens int64 `gorm:"column:total_output_tokens"` + TotalCacheTokens int64 `gorm:"column:total_cache_tokens"` + TotalCost float64 `gorm:"column:total_cost"` + TotalActualCost float64 `gorm:"column:total_actual_cost"` + AverageDurationMs float64 `gorm:"column:avg_duration_ms"` + } + + err := r.db.WithContext(ctx).Model(&model.UsageLog{}). + Select(` + COUNT(*) as total_requests, + COALESCE(SUM(input_tokens), 0) as total_input_tokens, + COALESCE(SUM(output_tokens), 0) as total_output_tokens, + COALESCE(SUM(cache_creation_tokens + cache_read_tokens), 0) as total_cache_tokens, + COALESCE(SUM(total_cost), 0) as total_cost, + COALESCE(SUM(actual_cost), 0) as total_actual_cost, + COALESCE(AVG(duration_ms), 0) as avg_duration_ms + `). + Where("created_at >= ? AND created_at <= ?", startTime, endTime). + Scan(&stats).Error + + if err != nil { + return nil, err + } + + return &UsageStats{ + TotalRequests: stats.TotalRequests, + TotalInputTokens: stats.TotalInputTokens, + TotalOutputTokens: stats.TotalOutputTokens, + TotalCacheTokens: stats.TotalCacheTokens, + TotalTokens: stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheTokens, + TotalCost: stats.TotalCost, + TotalActualCost: stats.TotalActualCost, + AverageDurationMs: stats.AverageDurationMs, + }, nil +} diff --git a/backend/internal/repository/user_repo.go b/backend/internal/repository/user_repo.go new file mode 100644 index 00000000..46de5390 --- /dev/null +++ b/backend/internal/repository/user_repo.go @@ -0,0 +1,130 @@ +package repository + +import ( + "context" + "sub2api/internal/model" + + "gorm.io/gorm" +) + +type UserRepository struct { + db *gorm.DB +} + +func NewUserRepository(db *gorm.DB) *UserRepository { + return &UserRepository{db: db} +} + +func (r *UserRepository) Create(ctx context.Context, user *model.User) error { + return r.db.WithContext(ctx).Create(user).Error +} + +func (r *UserRepository) GetByID(ctx context.Context, id int64) (*model.User, error) { + var user model.User + err := r.db.WithContext(ctx).First(&user, id).Error + if err != nil { + return nil, err + } + return &user, nil +} + +func (r *UserRepository) GetByEmail(ctx context.Context, email string) (*model.User, error) { + var user model.User + err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} + +func (r *UserRepository) Update(ctx context.Context, user *model.User) error { + return r.db.WithContext(ctx).Save(user).Error +} + +func (r *UserRepository) Delete(ctx context.Context, id int64) error { + return r.db.WithContext(ctx).Delete(&model.User{}, id).Error +} + +func (r *UserRepository) List(ctx context.Context, params PaginationParams) ([]model.User, *PaginationResult, error) { + return r.ListWithFilters(ctx, params, "", "", "") +} + +// ListWithFilters lists users with optional filtering by status, role, and search query +func (r *UserRepository) ListWithFilters(ctx context.Context, params PaginationParams, status, role, search string) ([]model.User, *PaginationResult, error) { + var users []model.User + var total int64 + + db := r.db.WithContext(ctx).Model(&model.User{}) + + // Apply filters + if status != "" { + db = db.Where("status = ?", status) + } + if role != "" { + db = db.Where("role = ?", role) + } + if search != "" { + searchPattern := "%" + search + "%" + db = db.Where("email ILIKE ?", searchPattern) + } + + if err := db.Count(&total).Error; err != nil { + return nil, nil, err + } + + if err := db.Offset(params.Offset()).Limit(params.Limit()).Order("id DESC").Find(&users).Error; err != nil { + return nil, nil, err + } + + pages := int(total) / params.Limit() + if int(total)%params.Limit() > 0 { + pages++ + } + + return users, &PaginationResult{ + Total: total, + Page: params.Page, + PageSize: params.Limit(), + Pages: pages, + }, nil +} + +func (r *UserRepository) UpdateBalance(ctx context.Context, id int64, amount float64) error { + return r.db.WithContext(ctx).Model(&model.User{}).Where("id = ?", id). + Update("balance", gorm.Expr("balance + ?", amount)).Error +} + +// DeductBalance 扣减用户余额,仅当余额充足时执行 +func (r *UserRepository) DeductBalance(ctx context.Context, id int64, amount float64) error { + result := r.db.WithContext(ctx).Model(&model.User{}). + Where("id = ? AND balance >= ?", id, amount). + Update("balance", gorm.Expr("balance - ?", amount)) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return gorm.ErrRecordNotFound // 余额不足或用户不存在 + } + return nil +} + +func (r *UserRepository) UpdateConcurrency(ctx context.Context, id int64, amount int) error { + return r.db.WithContext(ctx).Model(&model.User{}).Where("id = ?", id). + Update("concurrency", gorm.Expr("concurrency + ?", amount)).Error +} + +func (r *UserRepository) ExistsByEmail(ctx context.Context, email string) (bool, error) { + var count int64 + err := r.db.WithContext(ctx).Model(&model.User{}).Where("email = ?", email).Count(&count).Error + return count > 0, err +} + +// RemoveGroupFromAllowedGroups 从所有用户的 allowed_groups 数组中移除指定的分组ID +// 使用 PostgreSQL 的 array_remove 函数 +func (r *UserRepository) RemoveGroupFromAllowedGroups(ctx context.Context, groupID int64) (int64, error) { + result := r.db.WithContext(ctx).Model(&model.User{}). + Where("? = ANY(allowed_groups)", groupID). + Update("allowed_groups", gorm.Expr("array_remove(allowed_groups, ?)", groupID)) + return result.RowsAffected, result.Error +} + diff --git a/backend/internal/repository/user_subscription_repo.go b/backend/internal/repository/user_subscription_repo.go new file mode 100644 index 00000000..49840a86 --- /dev/null +++ b/backend/internal/repository/user_subscription_repo.go @@ -0,0 +1,322 @@ +package repository + +import ( + "context" + "time" + + "sub2api/internal/model" + + "gorm.io/gorm" +) + +// UserSubscriptionRepository 用户订阅仓库 +type UserSubscriptionRepository struct { + db *gorm.DB +} + +// NewUserSubscriptionRepository 创建用户订阅仓库 +func NewUserSubscriptionRepository(db *gorm.DB) *UserSubscriptionRepository { + return &UserSubscriptionRepository{db: db} +} + +// Create 创建订阅 +func (r *UserSubscriptionRepository) Create(ctx context.Context, sub *model.UserSubscription) error { + return r.db.WithContext(ctx).Create(sub).Error +} + +// GetByID 根据ID获取订阅 +func (r *UserSubscriptionRepository) GetByID(ctx context.Context, id int64) (*model.UserSubscription, error) { + var sub model.UserSubscription + err := r.db.WithContext(ctx). + Preload("User"). + Preload("Group"). + Preload("AssignedByUser"). + First(&sub, id).Error + if err != nil { + return nil, err + } + return &sub, nil +} + +// GetByUserIDAndGroupID 根据用户ID和分组ID获取订阅 +func (r *UserSubscriptionRepository) GetByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (*model.UserSubscription, error) { + var sub model.UserSubscription + err := r.db.WithContext(ctx). + Preload("Group"). + Where("user_id = ? AND group_id = ?", userID, groupID). + First(&sub).Error + if err != nil { + return nil, err + } + return &sub, nil +} + +// GetActiveByUserIDAndGroupID 获取用户对特定分组的有效订阅 +func (r *UserSubscriptionRepository) GetActiveByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (*model.UserSubscription, error) { + var sub model.UserSubscription + err := r.db.WithContext(ctx). + Preload("Group"). + Where("user_id = ? AND group_id = ? AND status = ? AND expires_at > ?", + userID, groupID, model.SubscriptionStatusActive, time.Now()). + First(&sub).Error + if err != nil { + return nil, err + } + return &sub, nil +} + +// Update 更新订阅 +func (r *UserSubscriptionRepository) Update(ctx context.Context, sub *model.UserSubscription) error { + sub.UpdatedAt = time.Now() + return r.db.WithContext(ctx).Save(sub).Error +} + +// Delete 删除订阅 +func (r *UserSubscriptionRepository) Delete(ctx context.Context, id int64) error { + return r.db.WithContext(ctx).Delete(&model.UserSubscription{}, id).Error +} + +// ListByUserID 获取用户的所有订阅 +func (r *UserSubscriptionRepository) ListByUserID(ctx context.Context, userID int64) ([]model.UserSubscription, error) { + var subs []model.UserSubscription + err := r.db.WithContext(ctx). + Preload("Group"). + Where("user_id = ?", userID). + Order("created_at DESC"). + Find(&subs).Error + return subs, err +} + +// ListActiveByUserID 获取用户的所有有效订阅 +func (r *UserSubscriptionRepository) ListActiveByUserID(ctx context.Context, userID int64) ([]model.UserSubscription, error) { + var subs []model.UserSubscription + err := r.db.WithContext(ctx). + Preload("Group"). + Where("user_id = ? AND status = ? AND expires_at > ?", + userID, model.SubscriptionStatusActive, time.Now()). + Order("created_at DESC"). + Find(&subs).Error + return subs, err +} + +// ListByGroupID 获取分组的所有订阅(分页) +func (r *UserSubscriptionRepository) ListByGroupID(ctx context.Context, groupID int64, params PaginationParams) ([]model.UserSubscription, *PaginationResult, error) { + var subs []model.UserSubscription + var total int64 + + query := r.db.WithContext(ctx).Model(&model.UserSubscription{}).Where("group_id = ?", groupID) + + if err := query.Count(&total).Error; err != nil { + return nil, nil, err + } + + err := query. + Preload("User"). + Preload("Group"). + Order("created_at DESC"). + Offset(params.Offset()). + Limit(params.Limit()). + Find(&subs).Error + if err != nil { + return nil, nil, err + } + + pages := int(total) / params.Limit() + if int(total)%params.Limit() > 0 { + pages++ + } + + return subs, &PaginationResult{ + Total: total, + Page: params.Page, + PageSize: params.Limit(), + Pages: pages, + }, nil +} + +// List 获取所有订阅(分页,支持筛选) +func (r *UserSubscriptionRepository) List(ctx context.Context, params PaginationParams, userID, groupID *int64, status string) ([]model.UserSubscription, *PaginationResult, error) { + var subs []model.UserSubscription + var total int64 + + query := r.db.WithContext(ctx).Model(&model.UserSubscription{}) + + if userID != nil { + query = query.Where("user_id = ?", *userID) + } + if groupID != nil { + query = query.Where("group_id = ?", *groupID) + } + if status != "" { + query = query.Where("status = ?", status) + } + + if err := query.Count(&total).Error; err != nil { + return nil, nil, err + } + + err := query. + Preload("User"). + Preload("Group"). + Preload("AssignedByUser"). + Order("created_at DESC"). + Offset(params.Offset()). + Limit(params.Limit()). + Find(&subs).Error + if err != nil { + return nil, nil, err + } + + pages := int(total) / params.Limit() + if int(total)%params.Limit() > 0 { + pages++ + } + + return subs, &PaginationResult{ + Total: total, + Page: params.Page, + PageSize: params.Limit(), + Pages: pages, + }, nil +} + +// IncrementUsage 增加使用量 +func (r *UserSubscriptionRepository) IncrementUsage(ctx context.Context, id int64, costUSD float64) error { + return r.db.WithContext(ctx).Model(&model.UserSubscription{}). + Where("id = ?", id). + Updates(map[string]interface{}{ + "daily_usage_usd": gorm.Expr("daily_usage_usd + ?", costUSD), + "weekly_usage_usd": gorm.Expr("weekly_usage_usd + ?", costUSD), + "monthly_usage_usd": gorm.Expr("monthly_usage_usd + ?", costUSD), + "updated_at": time.Now(), + }).Error +} + +// ResetDailyUsage 重置日使用量 +func (r *UserSubscriptionRepository) ResetDailyUsage(ctx context.Context, id int64, newWindowStart time.Time) error { + return r.db.WithContext(ctx).Model(&model.UserSubscription{}). + Where("id = ?", id). + Updates(map[string]interface{}{ + "daily_usage_usd": 0, + "daily_window_start": newWindowStart, + "updated_at": time.Now(), + }).Error +} + +// ResetWeeklyUsage 重置周使用量 +func (r *UserSubscriptionRepository) ResetWeeklyUsage(ctx context.Context, id int64, newWindowStart time.Time) error { + return r.db.WithContext(ctx).Model(&model.UserSubscription{}). + Where("id = ?", id). + Updates(map[string]interface{}{ + "weekly_usage_usd": 0, + "weekly_window_start": newWindowStart, + "updated_at": time.Now(), + }).Error +} + +// ResetMonthlyUsage 重置月使用量 +func (r *UserSubscriptionRepository) ResetMonthlyUsage(ctx context.Context, id int64, newWindowStart time.Time) error { + return r.db.WithContext(ctx).Model(&model.UserSubscription{}). + Where("id = ?", id). + Updates(map[string]interface{}{ + "monthly_usage_usd": 0, + "monthly_window_start": newWindowStart, + "updated_at": time.Now(), + }).Error +} + +// ActivateWindows 激活所有窗口(首次使用时) +func (r *UserSubscriptionRepository) ActivateWindows(ctx context.Context, id int64, activateTime time.Time) error { + return r.db.WithContext(ctx).Model(&model.UserSubscription{}). + Where("id = ?", id). + Updates(map[string]interface{}{ + "daily_window_start": activateTime, + "weekly_window_start": activateTime, + "monthly_window_start": activateTime, + "updated_at": time.Now(), + }).Error +} + +// UpdateStatus 更新订阅状态 +func (r *UserSubscriptionRepository) UpdateStatus(ctx context.Context, id int64, status string) error { + return r.db.WithContext(ctx).Model(&model.UserSubscription{}). + Where("id = ?", id). + Updates(map[string]interface{}{ + "status": status, + "updated_at": time.Now(), + }).Error +} + +// ExtendExpiry 延长订阅过期时间 +func (r *UserSubscriptionRepository) ExtendExpiry(ctx context.Context, id int64, newExpiresAt time.Time) error { + return r.db.WithContext(ctx).Model(&model.UserSubscription{}). + Where("id = ?", id). + Updates(map[string]interface{}{ + "expires_at": newExpiresAt, + "updated_at": time.Now(), + }).Error +} + +// UpdateNotes 更新订阅备注 +func (r *UserSubscriptionRepository) UpdateNotes(ctx context.Context, id int64, notes string) error { + return r.db.WithContext(ctx).Model(&model.UserSubscription{}). + Where("id = ?", id). + Updates(map[string]interface{}{ + "notes": notes, + "updated_at": time.Now(), + }).Error +} + +// ListExpired 获取所有已过期但状态仍为active的订阅 +func (r *UserSubscriptionRepository) ListExpired(ctx context.Context) ([]model.UserSubscription, error) { + var subs []model.UserSubscription + err := r.db.WithContext(ctx). + Where("status = ? AND expires_at <= ?", model.SubscriptionStatusActive, time.Now()). + Find(&subs).Error + return subs, err +} + +// BatchUpdateExpiredStatus 批量更新过期订阅状态 +func (r *UserSubscriptionRepository) BatchUpdateExpiredStatus(ctx context.Context) (int64, error) { + result := r.db.WithContext(ctx).Model(&model.UserSubscription{}). + Where("status = ? AND expires_at <= ?", model.SubscriptionStatusActive, time.Now()). + Updates(map[string]interface{}{ + "status": model.SubscriptionStatusExpired, + "updated_at": time.Now(), + }) + return result.RowsAffected, result.Error +} + +// ExistsByUserIDAndGroupID 检查用户是否已有该分组的订阅 +func (r *UserSubscriptionRepository) ExistsByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (bool, error) { + var count int64 + err := r.db.WithContext(ctx).Model(&model.UserSubscription{}). + Where("user_id = ? AND group_id = ?", userID, groupID). + Count(&count).Error + return count > 0, err +} + +// CountByGroupID 获取分组的订阅数量 +func (r *UserSubscriptionRepository) CountByGroupID(ctx context.Context, groupID int64) (int64, error) { + var count int64 + err := r.db.WithContext(ctx).Model(&model.UserSubscription{}). + Where("group_id = ?", groupID). + Count(&count).Error + return count, err +} + +// CountActiveByGroupID 获取分组的有效订阅数量 +func (r *UserSubscriptionRepository) CountActiveByGroupID(ctx context.Context, groupID int64) (int64, error) { + var count int64 + err := r.db.WithContext(ctx).Model(&model.UserSubscription{}). + Where("group_id = ? AND status = ? AND expires_at > ?", + groupID, model.SubscriptionStatusActive, time.Now()). + Count(&count).Error + return count, err +} + +// DeleteByGroupID 删除分组相关的所有订阅记录 +func (r *UserSubscriptionRepository) DeleteByGroupID(ctx context.Context, groupID int64) (int64, error) { + result := r.db.WithContext(ctx).Where("group_id = ?", groupID).Delete(&model.UserSubscription{}) + return result.RowsAffected, result.Error +} diff --git a/backend/internal/service/account_service.go b/backend/internal/service/account_service.go new file mode 100644 index 00000000..b9bf7329 --- /dev/null +++ b/backend/internal/service/account_service.go @@ -0,0 +1,284 @@ +package service + +import ( + "context" + "errors" + "fmt" + "sub2api/internal/model" + "sub2api/internal/repository" + + "gorm.io/gorm" +) + +var ( + ErrAccountNotFound = errors.New("account not found") +) + +// CreateAccountRequest 创建账号请求 +type CreateAccountRequest struct { + Name string `json:"name"` + Platform string `json:"platform"` + Type string `json:"type"` + Credentials map[string]interface{} `json:"credentials"` + Extra map[string]interface{} `json:"extra"` + ProxyID *int64 `json:"proxy_id"` + Concurrency int `json:"concurrency"` + Priority int `json:"priority"` + GroupIDs []int64 `json:"group_ids"` +} + +// UpdateAccountRequest 更新账号请求 +type UpdateAccountRequest struct { + Name *string `json:"name"` + Credentials *map[string]interface{} `json:"credentials"` + Extra *map[string]interface{} `json:"extra"` + ProxyID *int64 `json:"proxy_id"` + Concurrency *int `json:"concurrency"` + Priority *int `json:"priority"` + Status *string `json:"status"` + GroupIDs *[]int64 `json:"group_ids"` +} + +// AccountService 账号管理服务 +type AccountService struct { + accountRepo *repository.AccountRepository + groupRepo *repository.GroupRepository +} + +// NewAccountService 创建账号服务实例 +func NewAccountService(accountRepo *repository.AccountRepository, groupRepo *repository.GroupRepository) *AccountService { + return &AccountService{ + accountRepo: accountRepo, + groupRepo: groupRepo, + } +} + +// Create 创建账号 +func (s *AccountService) Create(ctx context.Context, req CreateAccountRequest) (*model.Account, error) { + // 验证分组是否存在(如果指定了分组) + if len(req.GroupIDs) > 0 { + for _, groupID := range req.GroupIDs { + _, err := s.groupRepo.GetByID(ctx, groupID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("group %d not found", groupID) + } + return nil, fmt.Errorf("get group: %w", err) + } + } + } + + // 创建账号 + account := &model.Account{ + Name: req.Name, + Platform: req.Platform, + Type: req.Type, + Credentials: req.Credentials, + Extra: req.Extra, + ProxyID: req.ProxyID, + Concurrency: req.Concurrency, + Priority: req.Priority, + Status: model.StatusActive, + } + + if err := s.accountRepo.Create(ctx, account); err != nil { + return nil, fmt.Errorf("create account: %w", err) + } + + // 绑定分组 + if len(req.GroupIDs) > 0 { + if err := s.accountRepo.BindGroups(ctx, account.ID, req.GroupIDs); err != nil { + return nil, fmt.Errorf("bind groups: %w", err) + } + } + + return account, nil +} + +// GetByID 根据ID获取账号 +func (s *AccountService) GetByID(ctx context.Context, id int64) (*model.Account, error) { + account, err := s.accountRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrAccountNotFound + } + return nil, fmt.Errorf("get account: %w", err) + } + return account, nil +} + +// List 获取账号列表 +func (s *AccountService) List(ctx context.Context, params repository.PaginationParams) ([]model.Account, *repository.PaginationResult, error) { + accounts, pagination, err := s.accountRepo.List(ctx, params) + if err != nil { + return nil, nil, fmt.Errorf("list accounts: %w", err) + } + return accounts, pagination, nil +} + +// ListByPlatform 根据平台获取账号列表 +func (s *AccountService) ListByPlatform(ctx context.Context, platform string) ([]model.Account, error) { + accounts, err := s.accountRepo.ListByPlatform(ctx, platform) + if err != nil { + return nil, fmt.Errorf("list accounts by platform: %w", err) + } + return accounts, nil +} + +// ListByGroup 根据分组获取账号列表 +func (s *AccountService) ListByGroup(ctx context.Context, groupID int64) ([]model.Account, error) { + accounts, err := s.accountRepo.ListByGroup(ctx, groupID) + if err != nil { + return nil, fmt.Errorf("list accounts by group: %w", err) + } + return accounts, nil +} + +// Update 更新账号 +func (s *AccountService) Update(ctx context.Context, id int64, req UpdateAccountRequest) (*model.Account, error) { + account, err := s.accountRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrAccountNotFound + } + return nil, fmt.Errorf("get account: %w", err) + } + + // 更新字段 + if req.Name != nil { + account.Name = *req.Name + } + + if req.Credentials != nil { + account.Credentials = *req.Credentials + } + + if req.Extra != nil { + account.Extra = *req.Extra + } + + if req.ProxyID != nil { + account.ProxyID = req.ProxyID + } + + if req.Concurrency != nil { + account.Concurrency = *req.Concurrency + } + + if req.Priority != nil { + account.Priority = *req.Priority + } + + if req.Status != nil { + account.Status = *req.Status + } + + if err := s.accountRepo.Update(ctx, account); err != nil { + return nil, fmt.Errorf("update account: %w", err) + } + + // 更新分组绑定 + if req.GroupIDs != nil { + // 验证分组是否存在 + for _, groupID := range *req.GroupIDs { + _, err := s.groupRepo.GetByID(ctx, groupID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("group %d not found", groupID) + } + return nil, fmt.Errorf("get group: %w", err) + } + } + + if err := s.accountRepo.BindGroups(ctx, account.ID, *req.GroupIDs); err != nil { + return nil, fmt.Errorf("bind groups: %w", err) + } + } + + return account, nil +} + +// Delete 删除账号 +func (s *AccountService) Delete(ctx context.Context, id int64) error { + // 检查账号是否存在 + _, err := s.accountRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrAccountNotFound + } + return fmt.Errorf("get account: %w", err) + } + + if err := s.accountRepo.Delete(ctx, id); err != nil { + return fmt.Errorf("delete account: %w", err) + } + + return nil +} + +// UpdateStatus 更新账号状态 +func (s *AccountService) UpdateStatus(ctx context.Context, id int64, status string, errorMessage string) error { + account, err := s.accountRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrAccountNotFound + } + return fmt.Errorf("get account: %w", err) + } + + account.Status = status + account.ErrorMessage = errorMessage + + if err := s.accountRepo.Update(ctx, account); err != nil { + return fmt.Errorf("update account: %w", err) + } + + return nil +} + +// UpdateLastUsed 更新最后使用时间 +func (s *AccountService) UpdateLastUsed(ctx context.Context, id int64) error { + if err := s.accountRepo.UpdateLastUsed(ctx, id); err != nil { + return fmt.Errorf("update last used: %w", err) + } + return nil +} + +// GetCredential 获取账号凭证(安全访问) +func (s *AccountService) GetCredential(ctx context.Context, id int64, key string) (string, error) { + account, err := s.accountRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return "", ErrAccountNotFound + } + return "", fmt.Errorf("get account: %w", err) + } + + return account.GetCredential(key), nil +} + +// TestCredentials 测试账号凭证是否有效(需要实现具体平台的测试逻辑) +func (s *AccountService) TestCredentials(ctx context.Context, id int64) error { + account, err := s.accountRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrAccountNotFound + } + return fmt.Errorf("get account: %w", err) + } + + // 根据平台执行不同的测试逻辑 + switch account.Platform { + case model.PlatformAnthropic: + // TODO: 测试Anthropic API凭证 + return nil + case model.PlatformOpenAI: + // TODO: 测试OpenAI API凭证 + return nil + case model.PlatformGemini: + // TODO: 测试Gemini API凭证 + return nil + default: + return fmt.Errorf("unsupported platform: %s", account.Platform) + } +} diff --git a/backend/internal/service/account_test_service.go b/backend/internal/service/account_test_service.go new file mode 100644 index 00000000..ae6a47ba --- /dev/null +++ b/backend/internal/service/account_test_service.go @@ -0,0 +1,314 @@ +package service + +import ( + "bufio" + "bytes" + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "sub2api/internal/repository" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +const ( + testClaudeAPIURL = "https://api.anthropic.com/v1/messages" + testModel = "claude-sonnet-4-5-20250929" +) + +// TestEvent represents a SSE event for account testing +type TestEvent struct { + Type string `json:"type"` + Text string `json:"text,omitempty"` + Model string `json:"model,omitempty"` + Success bool `json:"success,omitempty"` + Error string `json:"error,omitempty"` +} + +// AccountTestService handles account testing operations +type AccountTestService struct { + repos *repository.Repositories + oauthService *OAuthService + httpClient *http.Client +} + +// NewAccountTestService creates a new AccountTestService +func NewAccountTestService(repos *repository.Repositories, oauthService *OAuthService) *AccountTestService { + return &AccountTestService{ + repos: repos, + oauthService: oauthService, + httpClient: &http.Client{ + Timeout: 60 * time.Second, + }, + } +} + +// generateSessionString generates a Claude Code style session string +func generateSessionString() string { + bytes := make([]byte, 32) + rand.Read(bytes) + hex64 := hex.EncodeToString(bytes) + sessionUUID := uuid.New().String() + return fmt.Sprintf("user_%s_account__session_%s", hex64, sessionUUID) +} + +// createTestPayload creates a minimal test request payload for OAuth/Setup Token accounts +func createTestPayload() map[string]interface{} { + return map[string]interface{}{ + "model": testModel, + "messages": []map[string]interface{}{ + { + "role": "user", + "content": []map[string]interface{}{ + { + "type": "text", + "text": "hi", + "cache_control": map[string]string{ + "type": "ephemeral", + }, + }, + }, + }, + }, + "system": []map[string]interface{}{ + { + "type": "text", + "text": "You are Claude Code, Anthropic's official CLI for Claude.", + "cache_control": map[string]string{ + "type": "ephemeral", + }, + }, + }, + "metadata": map[string]string{ + "user_id": generateSessionString(), + }, + "max_tokens": 1024, + "temperature": 1, + "stream": true, + } +} + +// createApiKeyTestPayload creates a simpler test request payload for API Key accounts +func createApiKeyTestPayload(model string) map[string]interface{} { + return map[string]interface{}{ + "model": model, + "messages": []map[string]interface{}{ + { + "role": "user", + "content": "hi", + }, + }, + "max_tokens": 1024, + "stream": true, + } +} + +// TestAccountConnection tests an account's connection by sending a test request +func (s *AccountTestService) TestAccountConnection(c *gin.Context, accountID int64) error { + ctx := c.Request.Context() + + // Get account + account, err := s.repos.Account.GetByID(ctx, accountID) + if err != nil { + return s.sendErrorAndEnd(c, "Account not found") + } + + // Determine authentication method based on account type + var authToken string + var authType string // "bearer" for OAuth, "apikey" for API Key + var apiURL string + + if account.IsOAuth() { + // OAuth or Setup Token account + authType = "bearer" + apiURL = testClaudeAPIURL + authToken = account.GetCredential("access_token") + if authToken == "" { + return s.sendErrorAndEnd(c, "No access token available") + } + + // Check if token needs refresh + needRefresh := false + if expiresAtStr := account.GetCredential("expires_at"); expiresAtStr != "" { + expiresAt, err := strconv.ParseInt(expiresAtStr, 10, 64) + if err == nil && time.Now().Unix()+300 > expiresAt { // 5 minute buffer + needRefresh = true + } + } + + if needRefresh && s.oauthService != nil { + tokenInfo, err := s.oauthService.RefreshAccountToken(ctx, account) + if err != nil { + return s.sendErrorAndEnd(c, fmt.Sprintf("Failed to refresh token: %s", err.Error())) + } + authToken = tokenInfo.AccessToken + } + } else if account.Type == "apikey" { + // API Key account + authType = "apikey" + authToken = account.GetCredential("api_key") + if authToken == "" { + return s.sendErrorAndEnd(c, "No API key available") + } + + // Get base URL (use default if not set) + apiURL = account.GetBaseURL() + if apiURL == "" { + apiURL = "https://api.anthropic.com" + } + // Append /v1/messages endpoint + apiURL = strings.TrimSuffix(apiURL, "/") + "/v1/messages" + } else { + return s.sendErrorAndEnd(c, fmt.Sprintf("Unsupported account type: %s", account.Type)) + } + + // Set SSE headers + c.Writer.Header().Set("Content-Type", "text/event-stream") + c.Writer.Header().Set("Cache-Control", "no-cache") + c.Writer.Header().Set("Connection", "keep-alive") + c.Writer.Header().Set("X-Accel-Buffering", "no") + c.Writer.Flush() + + // Create test request payload + var payload map[string]interface{} + var actualModel string + if authType == "apikey" { + // Use simpler payload for API Key (without Claude Code specific fields) + // Apply model mapping if configured + actualModel = account.GetMappedModel(testModel) + payload = createApiKeyTestPayload(actualModel) + } else { + actualModel = testModel + payload = createTestPayload() + } + payloadBytes, _ := json.Marshal(payload) + + // Send test_start event with model info + s.sendEvent(c, TestEvent{Type: "test_start", Model: actualModel}) + + req, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewReader(payloadBytes)) + if err != nil { + return s.sendErrorAndEnd(c, "Failed to create request") + } + + // Set headers based on auth type + req.Header.Set("Content-Type", "application/json") + req.Header.Set("anthropic-version", "2023-06-01") + + if authType == "bearer" { + req.Header.Set("Authorization", "Bearer "+authToken) + req.Header.Set("anthropic-beta", "prompt-caching-2024-07-31,interleaved-thinking-2025-05-14,output-128k-2025-02-19") + } else { + // API Key uses x-api-key header + req.Header.Set("x-api-key", authToken) + } + + // Configure proxy if account has one + transport := http.DefaultTransport.(*http.Transport).Clone() + if account.ProxyID != nil && account.Proxy != nil { + proxyURL := account.Proxy.URL() + if proxyURL != "" { + if parsedURL, err := url.Parse(proxyURL); err == nil { + transport.Proxy = http.ProxyURL(parsedURL) + } + } + } + + client := &http.Client{ + Transport: transport, + Timeout: 60 * time.Second, + } + + resp, err := client.Do(req) + if err != nil { + return s.sendErrorAndEnd(c, fmt.Sprintf("Request failed: %s", err.Error())) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return s.sendErrorAndEnd(c, fmt.Sprintf("API returned %d: %s", resp.StatusCode, string(body))) + } + + // Process SSE stream + return s.processStream(c, resp.Body) +} + +// processStream processes the SSE stream from Claude API +func (s *AccountTestService) processStream(c *gin.Context, body io.Reader) error { + reader := bufio.NewReader(body) + + for { + line, err := reader.ReadString('\n') + if err != nil { + if err == io.EOF { + // Stream ended, send complete event + s.sendEvent(c, TestEvent{Type: "test_complete", Success: true}) + return nil + } + return s.sendErrorAndEnd(c, fmt.Sprintf("Stream read error: %s", err.Error())) + } + + line = strings.TrimSpace(line) + if line == "" || !strings.HasPrefix(line, "data: ") { + continue + } + + jsonStr := strings.TrimPrefix(line, "data: ") + if jsonStr == "[DONE]" { + s.sendEvent(c, TestEvent{Type: "test_complete", Success: true}) + return nil + } + + var data map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + continue + } + + eventType, _ := data["type"].(string) + + switch eventType { + case "content_block_delta": + if delta, ok := data["delta"].(map[string]interface{}); ok { + if text, ok := delta["text"].(string); ok { + s.sendEvent(c, TestEvent{Type: "content", Text: text}) + } + } + case "message_stop": + s.sendEvent(c, TestEvent{Type: "test_complete", Success: true}) + return nil + case "error": + errorMsg := "Unknown error" + if errData, ok := data["error"].(map[string]interface{}); ok { + if msg, ok := errData["message"].(string); ok { + errorMsg = msg + } + } + return s.sendErrorAndEnd(c, errorMsg) + } + } +} + +// sendEvent sends a SSE event to the client +func (s *AccountTestService) sendEvent(c *gin.Context, event TestEvent) { + eventJSON, _ := json.Marshal(event) + fmt.Fprintf(c.Writer, "data: %s\n\n", eventJSON) + c.Writer.Flush() +} + +// sendErrorAndEnd sends an error event and ends the stream +func (s *AccountTestService) sendErrorAndEnd(c *gin.Context, errorMsg string) error { + log.Printf("Account test error: %s", errorMsg) + s.sendEvent(c, TestEvent{Type: "error", Error: errorMsg}) + return fmt.Errorf(errorMsg) +} diff --git a/backend/internal/service/account_usage_service.go b/backend/internal/service/account_usage_service.go new file mode 100644 index 00000000..fe227fe6 --- /dev/null +++ b/backend/internal/service/account_usage_service.go @@ -0,0 +1,345 @@ +package service + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/url" + "sync" + "time" + + "sub2api/internal/model" + "sub2api/internal/repository" +) + +// usageCache 用于缓存usage数据 +type usageCache struct { + data *UsageInfo + timestamp time.Time +} + +var ( + usageCacheMap = sync.Map{} + cacheTTL = 10 * time.Minute +) + +// WindowStats 窗口期统计 +type WindowStats struct { + Requests int64 `json:"requests"` + Tokens int64 `json:"tokens"` + Cost float64 `json:"cost"` +} + +// UsageProgress 使用量进度 +type UsageProgress struct { + Utilization float64 `json:"utilization"` // 使用率百分比 (0-100+,100表示100%) + ResetsAt *time.Time `json:"resets_at"` // 重置时间 + RemainingSeconds int `json:"remaining_seconds"` // 距重置剩余秒数 + WindowStats *WindowStats `json:"window_stats,omitempty"` // 窗口期统计(从窗口开始到当前的使用量) +} + +// UsageInfo 账号使用量信息 +type UsageInfo struct { + UpdatedAt *time.Time `json:"updated_at,omitempty"` // 更新时间 + FiveHour *UsageProgress `json:"five_hour"` // 5小时窗口 + SevenDay *UsageProgress `json:"seven_day,omitempty"` // 7天窗口 + SevenDaySonnet *UsageProgress `json:"seven_day_sonnet,omitempty"` // 7天Sonnet窗口 +} + +// ClaudeUsageResponse Anthropic API返回的usage结构 +type ClaudeUsageResponse struct { + FiveHour struct { + Utilization float64 `json:"utilization"` + ResetsAt string `json:"resets_at"` + } `json:"five_hour"` + SevenDay struct { + Utilization float64 `json:"utilization"` + ResetsAt string `json:"resets_at"` + } `json:"seven_day"` + SevenDaySonnet struct { + Utilization float64 `json:"utilization"` + ResetsAt string `json:"resets_at"` + } `json:"seven_day_sonnet"` +} + +// AccountUsageService 账号使用量查询服务 +type AccountUsageService struct { + repos *repository.Repositories + oauthService *OAuthService + httpClient *http.Client +} + +// NewAccountUsageService 创建AccountUsageService实例 +func NewAccountUsageService(repos *repository.Repositories, oauthService *OAuthService) *AccountUsageService { + return &AccountUsageService{ + repos: repos, + oauthService: oauthService, + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + } +} + +// GetUsage 获取账号使用量 +// OAuth账号: 调用Anthropic API获取真实数据(需要profile scope),缓存10分钟 +// Setup Token账号: 根据session_window推算5h窗口,7d数据不可用(没有profile scope) +// API Key账号: 不支持usage查询 +func (s *AccountUsageService) GetUsage(ctx context.Context, accountID int64) (*UsageInfo, error) { + account, err := s.repos.Account.GetByID(ctx, accountID) + if err != nil { + return nil, fmt.Errorf("get account failed: %w", err) + } + + // 只有oauth类型账号可以通过API获取usage(有profile scope) + if account.CanGetUsage() { + // 检查缓存 + if cached, ok := usageCacheMap.Load(accountID); ok { + cache := cached.(*usageCache) + if time.Since(cache.timestamp) < cacheTTL { + return cache.data, nil + } + } + + // 从API获取数据 + usage, err := s.fetchOAuthUsage(ctx, account) + if err != nil { + return nil, err + } + + // 添加5h窗口统计数据 + s.addWindowStats(ctx, account, usage) + + // 缓存结果 + usageCacheMap.Store(accountID, &usageCache{ + data: usage, + timestamp: time.Now(), + }) + + return usage, nil + } + + // Setup Token账号:根据session_window推算(没有profile scope,无法调用usage API) + if account.Type == model.AccountTypeSetupToken { + usage := s.estimateSetupTokenUsage(account) + // 添加窗口统计 + s.addWindowStats(ctx, account, usage) + return usage, nil + } + + // API Key账号不支持usage查询 + return nil, fmt.Errorf("account type %s does not support usage query", account.Type) +} + +// addWindowStats 为usage数据添加窗口期统计 +func (s *AccountUsageService) addWindowStats(ctx context.Context, account *model.Account, usage *UsageInfo) { + if usage.FiveHour == nil { + return + } + + // 使用session_window_start作为统计起始时间 + var startTime time.Time + if account.SessionWindowStart != nil { + startTime = *account.SessionWindowStart + } else { + // 如果没有窗口信息,使用5小时前作为默认 + startTime = time.Now().Add(-5 * time.Hour) + } + + stats, err := s.repos.UsageLog.GetAccountWindowStats(ctx, account.ID, startTime) + if err != nil { + log.Printf("Failed to get window stats for account %d: %v", account.ID, err) + return + } + + usage.FiveHour.WindowStats = &WindowStats{ + Requests: stats.Requests, + Tokens: stats.Tokens, + Cost: stats.Cost, + } +} + +// GetTodayStats 获取账号今日统计 +func (s *AccountUsageService) GetTodayStats(ctx context.Context, accountID int64) (*WindowStats, error) { + stats, err := s.repos.UsageLog.GetAccountTodayStats(ctx, accountID) + if err != nil { + return nil, fmt.Errorf("get today stats failed: %w", err) + } + + return &WindowStats{ + Requests: stats.Requests, + Tokens: stats.Tokens, + Cost: stats.Cost, + }, nil +} + +// fetchOAuthUsage 从Anthropic API获取OAuth账号的使用量 +func (s *AccountUsageService) fetchOAuthUsage(ctx context.Context, account *model.Account) (*UsageInfo, error) { + // 获取access token(从credentials中获取) + accessToken := account.GetCredential("access_token") + if accessToken == "" { + return nil, fmt.Errorf("no access token available") + } + + // 获取代理配置 + transport := http.DefaultTransport.(*http.Transport).Clone() + if account.ProxyID != nil && account.Proxy != nil { + proxyURL := account.Proxy.URL() + if proxyURL != "" { + if parsedURL, err := url.Parse(proxyURL); err == nil { + transport.Proxy = http.ProxyURL(parsedURL) + } + } + } + + client := &http.Client{ + Transport: transport, + Timeout: 30 * time.Second, + } + + // 构建请求 + req, err := http.NewRequestWithContext(ctx, "GET", "https://api.anthropic.com/api/oauth/usage", nil) + if err != nil { + return nil, fmt.Errorf("create request failed: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+accessToken) + req.Header.Set("anthropic-beta", "oauth-2025-04-20") + + // 发送请求 + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body)) + } + + // 解析响应 + var usageResp ClaudeUsageResponse + if err := json.NewDecoder(resp.Body).Decode(&usageResp); err != nil { + return nil, fmt.Errorf("decode response failed: %w", err) + } + + // 转换为UsageInfo + now := time.Now() + return s.buildUsageInfo(&usageResp, &now), nil +} + +// parseTime 尝试多种格式解析时间 +func parseTime(s string) (time.Time, error) { + formats := []string{ + time.RFC3339, + time.RFC3339Nano, + "2006-01-02T15:04:05Z", + "2006-01-02T15:04:05.000Z", + } + for _, format := range formats { + if t, err := time.Parse(format, s); err == nil { + return t, nil + } + } + return time.Time{}, fmt.Errorf("unable to parse time: %s", s) +} + +// buildUsageInfo 构建UsageInfo +func (s *AccountUsageService) buildUsageInfo(resp *ClaudeUsageResponse, updatedAt *time.Time) *UsageInfo { + info := &UsageInfo{ + UpdatedAt: updatedAt, + } + + // 5小时窗口 + if resp.FiveHour.ResetsAt != "" { + if fiveHourReset, err := parseTime(resp.FiveHour.ResetsAt); err == nil { + info.FiveHour = &UsageProgress{ + Utilization: resp.FiveHour.Utilization, + ResetsAt: &fiveHourReset, + RemainingSeconds: int(time.Until(fiveHourReset).Seconds()), + } + } else { + log.Printf("Failed to parse FiveHour.ResetsAt: %s, error: %v", resp.FiveHour.ResetsAt, err) + // 即使解析失败也返回utilization + info.FiveHour = &UsageProgress{ + Utilization: resp.FiveHour.Utilization, + } + } + } + + // 7天窗口 + if resp.SevenDay.ResetsAt != "" { + if sevenDayReset, err := parseTime(resp.SevenDay.ResetsAt); err == nil { + info.SevenDay = &UsageProgress{ + Utilization: resp.SevenDay.Utilization, + ResetsAt: &sevenDayReset, + RemainingSeconds: int(time.Until(sevenDayReset).Seconds()), + } + } else { + log.Printf("Failed to parse SevenDay.ResetsAt: %s, error: %v", resp.SevenDay.ResetsAt, err) + info.SevenDay = &UsageProgress{ + Utilization: resp.SevenDay.Utilization, + } + } + } + + // 7天Sonnet窗口 + if resp.SevenDaySonnet.ResetsAt != "" { + if sonnetReset, err := parseTime(resp.SevenDaySonnet.ResetsAt); err == nil { + info.SevenDaySonnet = &UsageProgress{ + Utilization: resp.SevenDaySonnet.Utilization, + ResetsAt: &sonnetReset, + RemainingSeconds: int(time.Until(sonnetReset).Seconds()), + } + } else { + log.Printf("Failed to parse SevenDaySonnet.ResetsAt: %s, error: %v", resp.SevenDaySonnet.ResetsAt, err) + info.SevenDaySonnet = &UsageProgress{ + Utilization: resp.SevenDaySonnet.Utilization, + } + } + } + + return info +} + +// estimateSetupTokenUsage 根据session_window推算Setup Token账号的使用量 +func (s *AccountUsageService) estimateSetupTokenUsage(account *model.Account) *UsageInfo { + info := &UsageInfo{} + + // 如果有session_window信息 + if account.SessionWindowEnd != nil { + remaining := int(time.Until(*account.SessionWindowEnd).Seconds()) + if remaining < 0 { + remaining = 0 + } + + // 根据状态估算使用率 (百分比形式,100 = 100%) + var utilization float64 + switch account.SessionWindowStatus { + case "rejected": + utilization = 100.0 + case "allowed_warning": + utilization = 80.0 + default: + utilization = 0.0 + } + + info.FiveHour = &UsageProgress{ + Utilization: utilization, + ResetsAt: account.SessionWindowEnd, + RemainingSeconds: remaining, + } + } else { + // 没有窗口信息,返回空数据 + info.FiveHour = &UsageProgress{ + Utilization: 0, + RemainingSeconds: 0, + } + } + + // Setup Token无法获取7d数据 + return info +} diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go new file mode 100644 index 00000000..af8b67f5 --- /dev/null +++ b/backend/internal/service/admin_service.go @@ -0,0 +1,989 @@ +package service + +import ( + "context" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "time" + + "sub2api/internal/model" + "sub2api/internal/repository" + + "golang.org/x/net/proxy" + "gorm.io/gorm" +) + +// AdminService interface defines admin management operations +type AdminService interface { + // User management + ListUsers(ctx context.Context, page, pageSize int, status, role, search string) ([]model.User, int64, error) + GetUser(ctx context.Context, id int64) (*model.User, error) + CreateUser(ctx context.Context, input *CreateUserInput) (*model.User, error) + UpdateUser(ctx context.Context, id int64, input *UpdateUserInput) (*model.User, error) + DeleteUser(ctx context.Context, id int64) error + UpdateUserBalance(ctx context.Context, userID int64, balance float64, operation string) (*model.User, error) + GetUserAPIKeys(ctx context.Context, userID int64, page, pageSize int) ([]model.ApiKey, int64, error) + GetUserUsageStats(ctx context.Context, userID int64, period string) (interface{}, error) + + // Group management + ListGroups(ctx context.Context, page, pageSize int, platform, status string, isExclusive *bool) ([]model.Group, int64, error) + GetAllGroups(ctx context.Context) ([]model.Group, error) + GetAllGroupsByPlatform(ctx context.Context, platform string) ([]model.Group, error) + GetGroup(ctx context.Context, id int64) (*model.Group, error) + CreateGroup(ctx context.Context, input *CreateGroupInput) (*model.Group, error) + UpdateGroup(ctx context.Context, id int64, input *UpdateGroupInput) (*model.Group, error) + DeleteGroup(ctx context.Context, id int64) error + GetGroupAPIKeys(ctx context.Context, groupID int64, page, pageSize int) ([]model.ApiKey, int64, error) + + // Account management + ListAccounts(ctx context.Context, page, pageSize int, platform, accountType, status, search string) ([]model.Account, int64, error) + GetAccount(ctx context.Context, id int64) (*model.Account, error) + CreateAccount(ctx context.Context, input *CreateAccountInput) (*model.Account, error) + UpdateAccount(ctx context.Context, id int64, input *UpdateAccountInput) (*model.Account, error) + DeleteAccount(ctx context.Context, id int64) error + RefreshAccountCredentials(ctx context.Context, id int64) (*model.Account, error) + ClearAccountError(ctx context.Context, id int64) (*model.Account, error) + SetAccountSchedulable(ctx context.Context, id int64, schedulable bool) (*model.Account, error) + + // Proxy management + ListProxies(ctx context.Context, page, pageSize int, protocol, status, search string) ([]model.Proxy, int64, error) + GetAllProxies(ctx context.Context) ([]model.Proxy, error) + GetAllProxiesWithAccountCount(ctx context.Context) ([]model.ProxyWithAccountCount, error) + GetProxy(ctx context.Context, id int64) (*model.Proxy, error) + CreateProxy(ctx context.Context, input *CreateProxyInput) (*model.Proxy, error) + UpdateProxy(ctx context.Context, id int64, input *UpdateProxyInput) (*model.Proxy, error) + DeleteProxy(ctx context.Context, id int64) error + GetProxyAccounts(ctx context.Context, proxyID int64, page, pageSize int) ([]model.Account, int64, error) + CheckProxyExists(ctx context.Context, host string, port int, username, password string) (bool, error) + TestProxy(ctx context.Context, id int64) (*ProxyTestResult, error) + + // Redeem code management + ListRedeemCodes(ctx context.Context, page, pageSize int, codeType, status, search string) ([]model.RedeemCode, int64, error) + GetRedeemCode(ctx context.Context, id int64) (*model.RedeemCode, error) + GenerateRedeemCodes(ctx context.Context, input *GenerateRedeemCodesInput) ([]model.RedeemCode, error) + DeleteRedeemCode(ctx context.Context, id int64) error + BatchDeleteRedeemCodes(ctx context.Context, ids []int64) (int64, error) + ExpireRedeemCode(ctx context.Context, id int64) (*model.RedeemCode, error) +} + +// Input types for admin operations +type CreateUserInput struct { + Email string + Password string + Balance float64 + Concurrency int + AllowedGroups []int64 +} + +type UpdateUserInput struct { + Email string + Password string + Balance *float64 // 使用指针区分"未提供"和"设置为0" + Concurrency *int // 使用指针区分"未提供"和"设置为0" + Status string + AllowedGroups *[]int64 // 使用指针区分"未提供"和"设置为空数组" +} + +type CreateGroupInput struct { + Name string + Description string + Platform string + RateMultiplier float64 + IsExclusive bool + SubscriptionType string // standard/subscription + DailyLimitUSD *float64 // 日限额 (USD) + WeeklyLimitUSD *float64 // 周限额 (USD) + MonthlyLimitUSD *float64 // 月限额 (USD) +} + +type UpdateGroupInput struct { + Name string + Description string + Platform string + RateMultiplier *float64 // 使用指针以支持设置为0 + IsExclusive *bool + Status string + SubscriptionType string // standard/subscription + DailyLimitUSD *float64 // 日限额 (USD) + WeeklyLimitUSD *float64 // 周限额 (USD) + MonthlyLimitUSD *float64 // 月限额 (USD) +} + +type CreateAccountInput struct { + Name string + Platform string + Type string + Credentials map[string]interface{} + Extra map[string]interface{} + ProxyID *int64 + Concurrency int + Priority int + GroupIDs []int64 +} + +type UpdateAccountInput struct { + Name string + Type string // Account type: oauth, setup-token, apikey + Credentials map[string]interface{} + Extra map[string]interface{} + ProxyID *int64 + Concurrency *int // 使用指针区分"未提供"和"设置为0" + Priority *int // 使用指针区分"未提供"和"设置为0" + Status string + GroupIDs *[]int64 +} + +type CreateProxyInput struct { + Name string + Protocol string + Host string + Port int + Username string + Password string +} + +type UpdateProxyInput struct { + Name string + Protocol string + Host string + Port int + Username string + Password string + Status string +} + +type GenerateRedeemCodesInput struct { + Count int + Type string + Value float64 + GroupID *int64 // 订阅类型专用:关联的分组ID + ValidityDays int // 订阅类型专用:有效天数 +} + +// ProxyTestResult represents the result of testing a proxy +type ProxyTestResult struct { + Success bool `json:"success"` + Message string `json:"message"` + LatencyMs int64 `json:"latency_ms,omitempty"` + IPAddress string `json:"ip_address,omitempty"` + City string `json:"city,omitempty"` + Region string `json:"region,omitempty"` + Country string `json:"country,omitempty"` +} + +// adminServiceImpl implements AdminService +type adminServiceImpl struct { + userRepo *repository.UserRepository + groupRepo *repository.GroupRepository + accountRepo *repository.AccountRepository + proxyRepo *repository.ProxyRepository + apiKeyRepo *repository.ApiKeyRepository + redeemCodeRepo *repository.RedeemCodeRepository + usageLogRepo *repository.UsageLogRepository + userSubRepo *repository.UserSubscriptionRepository + billingCacheService *BillingCacheService +} + +// NewAdminService creates a new AdminService +func NewAdminService(repos *repository.Repositories) AdminService { + return &adminServiceImpl{ + userRepo: repos.User, + groupRepo: repos.Group, + accountRepo: repos.Account, + proxyRepo: repos.Proxy, + apiKeyRepo: repos.ApiKey, + redeemCodeRepo: repos.RedeemCode, + usageLogRepo: repos.UsageLog, + userSubRepo: repos.UserSubscription, + } +} + +// SetBillingCacheService 设置计费缓存服务(用于缓存失效) +// 注意:AdminService是接口,需要类型断言 +func SetAdminServiceBillingCache(adminService AdminService, billingCacheService *BillingCacheService) { + if impl, ok := adminService.(*adminServiceImpl); ok { + impl.billingCacheService = billingCacheService + } +} + +// User management implementations +func (s *adminServiceImpl) ListUsers(ctx context.Context, page, pageSize int, status, role, search string) ([]model.User, int64, error) { + params := repository.PaginationParams{Page: page, PageSize: pageSize} + users, result, err := s.userRepo.ListWithFilters(ctx, params, status, role, search) + if err != nil { + return nil, 0, err + } + return users, result.Total, nil +} + +func (s *adminServiceImpl) GetUser(ctx context.Context, id int64) (*model.User, error) { + return s.userRepo.GetByID(ctx, id) +} + +func (s *adminServiceImpl) CreateUser(ctx context.Context, input *CreateUserInput) (*model.User, error) { + user := &model.User{ + Email: input.Email, + Role: "user", // Always create as regular user, never admin + Balance: input.Balance, + Concurrency: input.Concurrency, + Status: model.StatusActive, + } + if err := user.SetPassword(input.Password); err != nil { + return nil, err + } + if err := s.userRepo.Create(ctx, user); err != nil { + return nil, err + } + return user, nil +} + +func (s *adminServiceImpl) UpdateUser(ctx context.Context, id int64, input *UpdateUserInput) (*model.User, error) { + user, err := s.userRepo.GetByID(ctx, id) + if err != nil { + return nil, err + } + + // Protect admin users: cannot disable admin accounts + if user.Role == "admin" && input.Status == "disabled" { + return nil, errors.New("cannot disable admin user") + } + + // Track balance and concurrency changes for logging + oldBalance := user.Balance + oldConcurrency := user.Concurrency + + if input.Email != "" { + user.Email = input.Email + } + if input.Password != "" { + if err := user.SetPassword(input.Password); err != nil { + return nil, err + } + } + // Role is not allowed to be changed via API to prevent privilege escalation + if input.Status != "" { + user.Status = input.Status + } + + // 只在指针非 nil 时更新 Balance(支持设置为 0) + if input.Balance != nil { + user.Balance = *input.Balance + } + + // 只在指针非 nil 时更新 Concurrency(支持设置为任意值) + if input.Concurrency != nil { + user.Concurrency = *input.Concurrency + } + + // 只在指针非 nil 时更新 AllowedGroups + if input.AllowedGroups != nil { + user.AllowedGroups = *input.AllowedGroups + } + + if err := s.userRepo.Update(ctx, user); err != nil { + return nil, err + } + + // 余额变化时失效缓存 + if input.Balance != nil && *input.Balance != oldBalance { + if s.billingCacheService != nil { + go func() { + cacheCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + s.billingCacheService.InvalidateUserBalance(cacheCtx, id) + }() + } + } + + // Create adjustment records for balance/concurrency changes + balanceDiff := user.Balance - oldBalance + if balanceDiff != 0 { + adjustmentRecord := &model.RedeemCode{ + Code: model.GenerateRedeemCode(), + Type: model.AdjustmentTypeAdminBalance, + Value: balanceDiff, + Status: model.StatusUsed, + UsedBy: &user.ID, + } + now := time.Now() + adjustmentRecord.UsedAt = &now + if err := s.redeemCodeRepo.Create(ctx, adjustmentRecord); err != nil { + // Log error but don't fail the update + // The user update has already succeeded + } + } + + concurrencyDiff := user.Concurrency - oldConcurrency + if concurrencyDiff != 0 { + adjustmentRecord := &model.RedeemCode{ + Code: model.GenerateRedeemCode(), + Type: model.AdjustmentTypeAdminConcurrency, + Value: float64(concurrencyDiff), + Status: model.StatusUsed, + UsedBy: &user.ID, + } + now := time.Now() + adjustmentRecord.UsedAt = &now + if err := s.redeemCodeRepo.Create(ctx, adjustmentRecord); err != nil { + // Log error but don't fail the update + // The user update has already succeeded + } + } + + return user, nil +} + +func (s *adminServiceImpl) DeleteUser(ctx context.Context, id int64) error { + // Protect admin users: cannot delete admin accounts + user, err := s.userRepo.GetByID(ctx, id) + if err != nil { + return err + } + if user.Role == "admin" { + return errors.New("cannot delete admin user") + } + return s.userRepo.Delete(ctx, id) +} + +func (s *adminServiceImpl) UpdateUserBalance(ctx context.Context, userID int64, balance float64, operation string) (*model.User, error) { + user, err := s.userRepo.GetByID(ctx, userID) + if err != nil { + return nil, err + } + + switch operation { + case "set": + user.Balance = balance + case "add": + user.Balance += balance + case "subtract": + user.Balance -= balance + } + + if err := s.userRepo.Update(ctx, user); err != nil { + return nil, err + } + + // 失效余额缓存 + if s.billingCacheService != nil { + go func() { + cacheCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + s.billingCacheService.InvalidateUserBalance(cacheCtx, userID) + }() + } + + return user, nil +} + +func (s *adminServiceImpl) GetUserAPIKeys(ctx context.Context, userID int64, page, pageSize int) ([]model.ApiKey, int64, error) { + params := repository.PaginationParams{Page: page, PageSize: pageSize} + keys, result, err := s.apiKeyRepo.ListByUserID(ctx, userID, params) + if err != nil { + return nil, 0, err + } + return keys, result.Total, nil +} + +func (s *adminServiceImpl) GetUserUsageStats(ctx context.Context, userID int64, period string) (interface{}, error) { + // Return mock data for now + return map[string]interface{}{ + "period": period, + "total_requests": 0, + "total_cost": 0.0, + "total_tokens": 0, + "avg_duration_ms": 0, + }, nil +} + +// Group management implementations +func (s *adminServiceImpl) ListGroups(ctx context.Context, page, pageSize int, platform, status string, isExclusive *bool) ([]model.Group, int64, error) { + params := repository.PaginationParams{Page: page, PageSize: pageSize} + groups, result, err := s.groupRepo.ListWithFilters(ctx, params, platform, status, isExclusive) + if err != nil { + return nil, 0, err + } + return groups, result.Total, nil +} + +func (s *adminServiceImpl) GetAllGroups(ctx context.Context) ([]model.Group, error) { + return s.groupRepo.ListActive(ctx) +} + +func (s *adminServiceImpl) GetAllGroupsByPlatform(ctx context.Context, platform string) ([]model.Group, error) { + return s.groupRepo.ListActiveByPlatform(ctx, platform) +} + +func (s *adminServiceImpl) GetGroup(ctx context.Context, id int64) (*model.Group, error) { + return s.groupRepo.GetByID(ctx, id) +} + +func (s *adminServiceImpl) CreateGroup(ctx context.Context, input *CreateGroupInput) (*model.Group, error) { + platform := input.Platform + if platform == "" { + platform = model.PlatformAnthropic + } + + subscriptionType := input.SubscriptionType + if subscriptionType == "" { + subscriptionType = model.SubscriptionTypeStandard + } + + group := &model.Group{ + Name: input.Name, + Description: input.Description, + Platform: platform, + RateMultiplier: input.RateMultiplier, + IsExclusive: input.IsExclusive, + Status: model.StatusActive, + SubscriptionType: subscriptionType, + DailyLimitUSD: input.DailyLimitUSD, + WeeklyLimitUSD: input.WeeklyLimitUSD, + MonthlyLimitUSD: input.MonthlyLimitUSD, + } + if err := s.groupRepo.Create(ctx, group); err != nil { + return nil, err + } + return group, nil +} + +func (s *adminServiceImpl) UpdateGroup(ctx context.Context, id int64, input *UpdateGroupInput) (*model.Group, error) { + group, err := s.groupRepo.GetByID(ctx, id) + if err != nil { + return nil, err + } + + if input.Name != "" { + group.Name = input.Name + } + if input.Description != "" { + group.Description = input.Description + } + if input.Platform != "" { + group.Platform = input.Platform + } + if input.RateMultiplier != nil { + group.RateMultiplier = *input.RateMultiplier + } + if input.IsExclusive != nil { + group.IsExclusive = *input.IsExclusive + } + if input.Status != "" { + group.Status = input.Status + } + + // 订阅相关字段 + if input.SubscriptionType != "" { + group.SubscriptionType = input.SubscriptionType + } + // 限额字段支持设置为nil(清除限额)或具体值 + if input.DailyLimitUSD != nil { + group.DailyLimitUSD = input.DailyLimitUSD + } + if input.WeeklyLimitUSD != nil { + group.WeeklyLimitUSD = input.WeeklyLimitUSD + } + if input.MonthlyLimitUSD != nil { + group.MonthlyLimitUSD = input.MonthlyLimitUSD + } + + if err := s.groupRepo.Update(ctx, group); err != nil { + return nil, err + } + return group, nil +} + +func (s *adminServiceImpl) DeleteGroup(ctx context.Context, id int64) error { + // 先获取分组信息,检查是否存在 + group, err := s.groupRepo.GetByID(ctx, id) + if err != nil { + return fmt.Errorf("group not found: %w", err) + } + + // 订阅类型分组:先获取受影响的用户ID列表(用于事务后失效缓存) + var affectedUserIDs []int64 + if group.IsSubscriptionType() && s.billingCacheService != nil { + var subscriptions []model.UserSubscription + if err := s.groupRepo.DB().WithContext(ctx). + Where("group_id = ?", id). + Select("user_id"). + Find(&subscriptions).Error; err == nil { + for _, sub := range subscriptions { + affectedUserIDs = append(affectedUserIDs, sub.UserID) + } + } + } + + // 使用事务处理所有级联删除 + db := s.groupRepo.DB() + err = db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + // 1. 如果是订阅类型分组,删除 user_subscriptions 中的相关记录 + if group.IsSubscriptionType() { + if err := tx.Where("group_id = ?", id).Delete(&model.UserSubscription{}).Error; err != nil { + return fmt.Errorf("delete user subscriptions: %w", err) + } + } + + // 2. 将 api_keys 中绑定该分组的 group_id 设为 nil(任何类型的分组都需要) + if err := tx.Model(&model.ApiKey{}).Where("group_id = ?", id).Update("group_id", nil).Error; err != nil { + return fmt.Errorf("clear api key group_id: %w", err) + } + + // 3. 从 users.allowed_groups 数组中移除该分组 ID + if err := tx.Model(&model.User{}). + Where("? = ANY(allowed_groups)", id). + Update("allowed_groups", gorm.Expr("array_remove(allowed_groups, ?)", id)).Error; err != nil { + return fmt.Errorf("remove from allowed_groups: %w", err) + } + + // 4. 删除 account_groups 中间表的数据 + if err := tx.Where("group_id = ?", id).Delete(&model.AccountGroup{}).Error; err != nil { + return fmt.Errorf("delete account groups: %w", err) + } + + // 5. 删除分组本身 + if err := tx.Delete(&model.Group{}, id).Error; err != nil { + return fmt.Errorf("delete group: %w", err) + } + + return nil + }) + + if err != nil { + return err + } + + // 事务成功后,异步失效受影响用户的订阅缓存 + if len(affectedUserIDs) > 0 && s.billingCacheService != nil { + groupID := id + go func() { + cacheCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + for _, userID := range affectedUserIDs { + s.billingCacheService.InvalidateSubscription(cacheCtx, userID, groupID) + } + }() + } + + return nil +} + +func (s *adminServiceImpl) GetGroupAPIKeys(ctx context.Context, groupID int64, page, pageSize int) ([]model.ApiKey, int64, error) { + params := repository.PaginationParams{Page: page, PageSize: pageSize} + keys, result, err := s.apiKeyRepo.ListByGroupID(ctx, groupID, params) + if err != nil { + return nil, 0, err + } + return keys, result.Total, nil +} + +// Account management implementations +func (s *adminServiceImpl) ListAccounts(ctx context.Context, page, pageSize int, platform, accountType, status, search string) ([]model.Account, int64, error) { + params := repository.PaginationParams{Page: page, PageSize: pageSize} + accounts, result, err := s.accountRepo.ListWithFilters(ctx, params, platform, accountType, status, search) + if err != nil { + return nil, 0, err + } + return accounts, result.Total, nil +} + +func (s *adminServiceImpl) GetAccount(ctx context.Context, id int64) (*model.Account, error) { + return s.accountRepo.GetByID(ctx, id) +} + +func (s *adminServiceImpl) CreateAccount(ctx context.Context, input *CreateAccountInput) (*model.Account, error) { + account := &model.Account{ + Name: input.Name, + Platform: input.Platform, + Type: input.Type, + Credentials: model.JSONB(input.Credentials), + Extra: model.JSONB(input.Extra), + ProxyID: input.ProxyID, + Concurrency: input.Concurrency, + Priority: input.Priority, + Status: model.StatusActive, + } + if err := s.accountRepo.Create(ctx, account); err != nil { + return nil, err + } + // 绑定分组 + if len(input.GroupIDs) > 0 { + if err := s.accountRepo.BindGroups(ctx, account.ID, input.GroupIDs); err != nil { + return nil, err + } + } + return account, nil +} + +func (s *adminServiceImpl) UpdateAccount(ctx context.Context, id int64, input *UpdateAccountInput) (*model.Account, error) { + account, err := s.accountRepo.GetByID(ctx, id) + if err != nil { + return nil, err + } + + if input.Name != "" { + account.Name = input.Name + } + if input.Type != "" { + account.Type = input.Type + } + if input.Credentials != nil && len(input.Credentials) > 0 { + account.Credentials = model.JSONB(input.Credentials) + } + if input.Extra != nil && len(input.Extra) > 0 { + account.Extra = model.JSONB(input.Extra) + } + if input.ProxyID != nil { + account.ProxyID = input.ProxyID + } + // 只在指针非 nil 时更新 Concurrency(支持设置为 0) + if input.Concurrency != nil { + account.Concurrency = *input.Concurrency + } + // 只在指针非 nil 时更新 Priority(支持设置为 0) + if input.Priority != nil { + account.Priority = *input.Priority + } + if input.Status != "" { + account.Status = input.Status + } + + if err := s.accountRepo.Update(ctx, account); err != nil { + return nil, err + } + + // 更新分组绑定 + if input.GroupIDs != nil { + if err := s.accountRepo.BindGroups(ctx, account.ID, *input.GroupIDs); err != nil { + return nil, err + } + } + + return account, nil +} + +func (s *adminServiceImpl) DeleteAccount(ctx context.Context, id int64) error { + return s.accountRepo.Delete(ctx, id) +} + +func (s *adminServiceImpl) RefreshAccountCredentials(ctx context.Context, id int64) (*model.Account, error) { + account, err := s.accountRepo.GetByID(ctx, id) + if err != nil { + return nil, err + } + // TODO: Implement refresh logic + return account, nil +} + +func (s *adminServiceImpl) ClearAccountError(ctx context.Context, id int64) (*model.Account, error) { + account, err := s.accountRepo.GetByID(ctx, id) + if err != nil { + return nil, err + } + account.Status = model.StatusActive + account.ErrorMessage = "" + if err := s.accountRepo.Update(ctx, account); err != nil { + return nil, err + } + return account, nil +} + +func (s *adminServiceImpl) SetAccountSchedulable(ctx context.Context, id int64, schedulable bool) (*model.Account, error) { + if err := s.accountRepo.SetSchedulable(ctx, id, schedulable); err != nil { + return nil, err + } + return s.accountRepo.GetByID(ctx, id) +} + +// Proxy management implementations +func (s *adminServiceImpl) ListProxies(ctx context.Context, page, pageSize int, protocol, status, search string) ([]model.Proxy, int64, error) { + params := repository.PaginationParams{Page: page, PageSize: pageSize} + proxies, result, err := s.proxyRepo.ListWithFilters(ctx, params, protocol, status, search) + if err != nil { + return nil, 0, err + } + return proxies, result.Total, nil +} + +func (s *adminServiceImpl) GetAllProxies(ctx context.Context) ([]model.Proxy, error) { + return s.proxyRepo.ListActive(ctx) +} + +func (s *adminServiceImpl) GetAllProxiesWithAccountCount(ctx context.Context) ([]model.ProxyWithAccountCount, error) { + return s.proxyRepo.ListActiveWithAccountCount(ctx) +} + +func (s *adminServiceImpl) GetProxy(ctx context.Context, id int64) (*model.Proxy, error) { + return s.proxyRepo.GetByID(ctx, id) +} + +func (s *adminServiceImpl) CreateProxy(ctx context.Context, input *CreateProxyInput) (*model.Proxy, error) { + proxy := &model.Proxy{ + Name: input.Name, + Protocol: input.Protocol, + Host: input.Host, + Port: input.Port, + Username: input.Username, + Password: input.Password, + Status: model.StatusActive, + } + if err := s.proxyRepo.Create(ctx, proxy); err != nil { + return nil, err + } + return proxy, nil +} + +func (s *adminServiceImpl) UpdateProxy(ctx context.Context, id int64, input *UpdateProxyInput) (*model.Proxy, error) { + proxy, err := s.proxyRepo.GetByID(ctx, id) + if err != nil { + return nil, err + } + + if input.Name != "" { + proxy.Name = input.Name + } + if input.Protocol != "" { + proxy.Protocol = input.Protocol + } + if input.Host != "" { + proxy.Host = input.Host + } + if input.Port != 0 { + proxy.Port = input.Port + } + if input.Username != "" { + proxy.Username = input.Username + } + if input.Password != "" { + proxy.Password = input.Password + } + if input.Status != "" { + proxy.Status = input.Status + } + + if err := s.proxyRepo.Update(ctx, proxy); err != nil { + return nil, err + } + return proxy, nil +} + +func (s *adminServiceImpl) DeleteProxy(ctx context.Context, id int64) error { + return s.proxyRepo.Delete(ctx, id) +} + +func (s *adminServiceImpl) GetProxyAccounts(ctx context.Context, proxyID int64, page, pageSize int) ([]model.Account, int64, error) { + // Return mock data for now - would need a dedicated repository method + return []model.Account{}, 0, nil +} + +func (s *adminServiceImpl) CheckProxyExists(ctx context.Context, host string, port int, username, password string) (bool, error) { + return s.proxyRepo.ExistsByHostPortAuth(ctx, host, port, username, password) +} + +// Redeem code management implementations +func (s *adminServiceImpl) ListRedeemCodes(ctx context.Context, page, pageSize int, codeType, status, search string) ([]model.RedeemCode, int64, error) { + params := repository.PaginationParams{Page: page, PageSize: pageSize} + codes, result, err := s.redeemCodeRepo.ListWithFilters(ctx, params, codeType, status, search) + if err != nil { + return nil, 0, err + } + return codes, result.Total, nil +} + +func (s *adminServiceImpl) GetRedeemCode(ctx context.Context, id int64) (*model.RedeemCode, error) { + return s.redeemCodeRepo.GetByID(ctx, id) +} + +func (s *adminServiceImpl) GenerateRedeemCodes(ctx context.Context, input *GenerateRedeemCodesInput) ([]model.RedeemCode, error) { + // 如果是订阅类型,验证必须有 GroupID + if input.Type == model.RedeemTypeSubscription { + if input.GroupID == nil { + return nil, errors.New("group_id is required for subscription type") + } + // 验证分组存在且为订阅类型 + group, err := s.groupRepo.GetByID(ctx, *input.GroupID) + if err != nil { + return nil, fmt.Errorf("group not found: %w", err) + } + if !group.IsSubscriptionType() { + return nil, errors.New("group must be subscription type") + } + } + + codes := make([]model.RedeemCode, 0, input.Count) + for i := 0; i < input.Count; i++ { + code := model.RedeemCode{ + Code: model.GenerateRedeemCode(), + Type: input.Type, + Value: input.Value, + Status: model.StatusUnused, + } + // 订阅类型专用字段 + if input.Type == model.RedeemTypeSubscription { + code.GroupID = input.GroupID + code.ValidityDays = input.ValidityDays + if code.ValidityDays <= 0 { + code.ValidityDays = 30 // 默认30天 + } + } + if err := s.redeemCodeRepo.Create(ctx, &code); err != nil { + return nil, err + } + codes = append(codes, code) + } + return codes, nil +} + +func (s *adminServiceImpl) DeleteRedeemCode(ctx context.Context, id int64) error { + return s.redeemCodeRepo.Delete(ctx, id) +} + +func (s *adminServiceImpl) BatchDeleteRedeemCodes(ctx context.Context, ids []int64) (int64, error) { + var deleted int64 + for _, id := range ids { + if err := s.redeemCodeRepo.Delete(ctx, id); err == nil { + deleted++ + } + } + return deleted, nil +} + +func (s *adminServiceImpl) ExpireRedeemCode(ctx context.Context, id int64) (*model.RedeemCode, error) { + code, err := s.redeemCodeRepo.GetByID(ctx, id) + if err != nil { + return nil, err + } + code.Status = model.StatusExpired + if err := s.redeemCodeRepo.Update(ctx, code); err != nil { + return nil, err + } + return code, nil +} + +func (s *adminServiceImpl) TestProxy(ctx context.Context, id int64) (*ProxyTestResult, error) { + proxy, err := s.proxyRepo.GetByID(ctx, id) + if err != nil { + return nil, err + } + + return testProxyConnection(ctx, proxy) +} + +// testProxyConnection tests proxy connectivity by requesting ipinfo.io/json +func testProxyConnection(ctx context.Context, proxy *model.Proxy) (*ProxyTestResult, error) { + proxyURL := proxy.URL() + + // Create HTTP client with proxy + transport, err := createProxyTransport(proxyURL) + if err != nil { + return &ProxyTestResult{ + Success: false, + Message: fmt.Sprintf("Failed to create proxy transport: %v", err), + }, nil + } + + client := &http.Client{ + Transport: transport, + Timeout: 15 * time.Second, + } + + // Measure latency + startTime := time.Now() + + req, err := http.NewRequestWithContext(ctx, "GET", "https://ipinfo.io/json", nil) + if err != nil { + return &ProxyTestResult{ + Success: false, + Message: fmt.Sprintf("Failed to create request: %v", err), + }, nil + } + + resp, err := client.Do(req) + if err != nil { + return &ProxyTestResult{ + Success: false, + Message: fmt.Sprintf("Proxy connection failed: %v", err), + }, nil + } + defer resp.Body.Close() + + latencyMs := time.Since(startTime).Milliseconds() + + if resp.StatusCode != http.StatusOK { + return &ProxyTestResult{ + Success: false, + Message: fmt.Sprintf("Request failed with status: %d", resp.StatusCode), + LatencyMs: latencyMs, + }, nil + } + + // Parse ipinfo.io response + var ipInfo struct { + IP string `json:"ip"` + City string `json:"city"` + Region string `json:"region"` + Country string `json:"country"` + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return &ProxyTestResult{ + Success: true, + Message: "Proxy is accessible but failed to read response", + LatencyMs: latencyMs, + }, nil + } + + if err := json.Unmarshal(body, &ipInfo); err != nil { + return &ProxyTestResult{ + Success: true, + Message: "Proxy is accessible but failed to parse response", + LatencyMs: latencyMs, + }, nil + } + + return &ProxyTestResult{ + Success: true, + Message: "Proxy is accessible", + LatencyMs: latencyMs, + IPAddress: ipInfo.IP, + City: ipInfo.City, + Region: ipInfo.Region, + Country: ipInfo.Country, + }, nil +} + +// createProxyTransport creates an HTTP transport with the given proxy URL +func createProxyTransport(proxyURL string) (*http.Transport, error) { + parsedURL, err := url.Parse(proxyURL) + if err != nil { + return nil, fmt.Errorf("invalid proxy URL: %w", err) + } + + transport := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + switch parsedURL.Scheme { + case "http", "https": + transport.Proxy = http.ProxyURL(parsedURL) + case "socks5": + dialer, err := proxy.FromURL(parsedURL, proxy.Direct) + if err != nil { + return nil, fmt.Errorf("failed to create socks5 dialer: %w", err) + } + transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + return dialer.Dial(network, addr) + } + default: + return nil, fmt.Errorf("unsupported proxy protocol: %s", parsedURL.Scheme) + } + + return transport, nil +} diff --git a/backend/internal/service/api_key_service.go b/backend/internal/service/api_key_service.go new file mode 100644 index 00000000..b0830d3e --- /dev/null +++ b/backend/internal/service/api_key_service.go @@ -0,0 +1,464 @@ +package service + +import ( + "context" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "sub2api/internal/config" + "sub2api/internal/model" + "sub2api/internal/pkg/timezone" + "sub2api/internal/repository" + "time" + + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +var ( + ErrApiKeyNotFound = errors.New("api key not found") + ErrGroupNotAllowed = errors.New("user is not allowed to bind this group") + ErrApiKeyExists = errors.New("api key already exists") + ErrApiKeyTooShort = errors.New("api key must be at least 16 characters") + ErrApiKeyInvalidChars = errors.New("api key can only contain letters, numbers, underscores, and hyphens") + ErrApiKeyRateLimited = errors.New("too many failed attempts, please try again later") +) + +const ( + apiKeyRateLimitKeyPrefix = "apikey:create_rate_limit:" + apiKeyMaxErrorsPerHour = 20 + apiKeyRateLimitDuration = time.Hour +) + +// CreateApiKeyRequest 创建API Key请求 +type CreateApiKeyRequest struct { + Name string `json:"name"` + GroupID *int64 `json:"group_id"` + CustomKey *string `json:"custom_key"` // 可选的自定义key +} + +// UpdateApiKeyRequest 更新API Key请求 +type UpdateApiKeyRequest struct { + Name *string `json:"name"` + GroupID *int64 `json:"group_id"` + Status *string `json:"status"` +} + +// ApiKeyService API Key服务 +type ApiKeyService struct { + apiKeyRepo *repository.ApiKeyRepository + userRepo *repository.UserRepository + groupRepo *repository.GroupRepository + userSubRepo *repository.UserSubscriptionRepository + rdb *redis.Client + cfg *config.Config +} + +// NewApiKeyService 创建API Key服务实例 +func NewApiKeyService( + apiKeyRepo *repository.ApiKeyRepository, + userRepo *repository.UserRepository, + groupRepo *repository.GroupRepository, + userSubRepo *repository.UserSubscriptionRepository, + rdb *redis.Client, + cfg *config.Config, +) *ApiKeyService { + return &ApiKeyService{ + apiKeyRepo: apiKeyRepo, + userRepo: userRepo, + groupRepo: groupRepo, + userSubRepo: userSubRepo, + rdb: rdb, + cfg: cfg, + } +} + +// GenerateKey 生成随机API Key +func (s *ApiKeyService) GenerateKey() (string, error) { + // 生成32字节随机数据 + bytes := make([]byte, 32) + if _, err := rand.Read(bytes); err != nil { + return "", fmt.Errorf("generate random bytes: %w", err) + } + + // 转换为十六进制字符串并添加前缀 + prefix := s.cfg.Default.ApiKeyPrefix + if prefix == "" { + prefix = "sk-" + } + + key := prefix + hex.EncodeToString(bytes) + return key, nil +} + +// ValidateCustomKey 验证自定义API Key格式 +func (s *ApiKeyService) ValidateCustomKey(key string) error { + // 检查长度 + if len(key) < 16 { + return ErrApiKeyTooShort + } + + // 检查字符:只允许字母、数字、下划线、连字符 + for _, c := range key { + if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_' || c == '-') { + return ErrApiKeyInvalidChars + } + } + + return nil +} + +// checkApiKeyRateLimit 检查用户创建自定义Key的错误次数是否超限 +func (s *ApiKeyService) checkApiKeyRateLimit(ctx context.Context, userID int64) error { + if s.rdb == nil { + return nil + } + + key := fmt.Sprintf("%s%d", apiKeyRateLimitKeyPrefix, userID) + + count, err := s.rdb.Get(ctx, key).Int() + if err != nil && !errors.Is(err, redis.Nil) { + // Redis 出错时不阻止用户操作 + return nil + } + + if count >= apiKeyMaxErrorsPerHour { + return ErrApiKeyRateLimited + } + + return nil +} + +// incrementApiKeyErrorCount 增加用户创建自定义Key的错误计数 +func (s *ApiKeyService) incrementApiKeyErrorCount(ctx context.Context, userID int64) { + if s.rdb == nil { + return + } + + key := fmt.Sprintf("%s%d", apiKeyRateLimitKeyPrefix, userID) + + pipe := s.rdb.Pipeline() + pipe.Incr(ctx, key) + pipe.Expire(ctx, key, apiKeyRateLimitDuration) + _, _ = pipe.Exec(ctx) +} + +// canUserBindGroup 检查用户是否可以绑定指定分组 +// 对于订阅类型分组:检查用户是否有有效订阅 +// 对于标准类型分组:使用原有的 AllowedGroups 和 IsExclusive 逻辑 +func (s *ApiKeyService) canUserBindGroup(ctx context.Context, user *model.User, group *model.Group) bool { + // 订阅类型分组:需要有效订阅 + if group.IsSubscriptionType() { + _, err := s.userSubRepo.GetActiveByUserIDAndGroupID(ctx, user.ID, group.ID) + return err == nil // 有有效订阅则允许 + } + // 标准类型分组:使用原有逻辑 + return user.CanBindGroup(group.ID, group.IsExclusive) +} + +// Create 创建API Key +func (s *ApiKeyService) Create(ctx context.Context, userID int64, req CreateApiKeyRequest) (*model.ApiKey, error) { + // 验证用户存在 + user, err := s.userRepo.GetByID(ctx, userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrUserNotFound + } + return nil, fmt.Errorf("get user: %w", err) + } + + // 验证分组权限(如果指定了分组) + if req.GroupID != nil { + group, err := s.groupRepo.GetByID(ctx, *req.GroupID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("group not found") + } + return nil, fmt.Errorf("get group: %w", err) + } + + // 检查用户是否可以绑定该分组 + if !s.canUserBindGroup(ctx, user, group) { + return nil, ErrGroupNotAllowed + } + } + + var key string + + // 判断是否使用自定义Key + if req.CustomKey != nil && *req.CustomKey != "" { + // 检查限流(仅对自定义key进行限流) + if err := s.checkApiKeyRateLimit(ctx, userID); err != nil { + return nil, err + } + + // 验证自定义Key格式 + if err := s.ValidateCustomKey(*req.CustomKey); err != nil { + return nil, err + } + + // 检查Key是否已存在 + exists, err := s.apiKeyRepo.ExistsByKey(ctx, *req.CustomKey) + if err != nil { + return nil, fmt.Errorf("check key exists: %w", err) + } + if exists { + // Key已存在,增加错误计数 + s.incrementApiKeyErrorCount(ctx, userID) + return nil, ErrApiKeyExists + } + + key = *req.CustomKey + } else { + // 生成随机API Key + var err error + key, err = s.GenerateKey() + if err != nil { + return nil, fmt.Errorf("generate key: %w", err) + } + } + + // 创建API Key记录 + apiKey := &model.ApiKey{ + UserID: userID, + Key: key, + Name: req.Name, + GroupID: req.GroupID, + Status: model.StatusActive, + } + + if err := s.apiKeyRepo.Create(ctx, apiKey); err != nil { + return nil, fmt.Errorf("create api key: %w", err) + } + + return apiKey, nil +} + +// List 获取用户的API Key列表 +func (s *ApiKeyService) List(ctx context.Context, userID int64, params repository.PaginationParams) ([]model.ApiKey, *repository.PaginationResult, error) { + keys, pagination, err := s.apiKeyRepo.ListByUserID(ctx, userID, params) + if err != nil { + return nil, nil, fmt.Errorf("list api keys: %w", err) + } + return keys, pagination, nil +} + +// GetByID 根据ID获取API Key +func (s *ApiKeyService) GetByID(ctx context.Context, id int64) (*model.ApiKey, error) { + apiKey, err := s.apiKeyRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrApiKeyNotFound + } + return nil, fmt.Errorf("get api key: %w", err) + } + return apiKey, nil +} + +// GetByKey 根据Key字符串获取API Key(用于认证) +func (s *ApiKeyService) GetByKey(ctx context.Context, key string) (*model.ApiKey, error) { + // 尝试从Redis缓存获取 + cacheKey := fmt.Sprintf("apikey:%s", key) + + // 这里可以添加Redis缓存逻辑,暂时直接查询数据库 + apiKey, err := s.apiKeyRepo.GetByKey(ctx, key) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrApiKeyNotFound + } + return nil, fmt.Errorf("get api key: %w", err) + } + + // 缓存到Redis(可选,TTL设置为5分钟) + if s.rdb != nil { + // 这里可以序列化并缓存API Key + _ = cacheKey // 使用变量避免未使用错误 + } + + return apiKey, nil +} + +// Update 更新API Key +func (s *ApiKeyService) Update(ctx context.Context, id int64, userID int64, req UpdateApiKeyRequest) (*model.ApiKey, error) { + apiKey, err := s.apiKeyRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrApiKeyNotFound + } + return nil, fmt.Errorf("get api key: %w", err) + } + + // 验证所有权 + if apiKey.UserID != userID { + return nil, ErrInsufficientPerms + } + + // 更新字段 + if req.Name != nil { + apiKey.Name = *req.Name + } + + if req.GroupID != nil { + // 验证分组权限 + user, err := s.userRepo.GetByID(ctx, userID) + if err != nil { + return nil, fmt.Errorf("get user: %w", err) + } + + group, err := s.groupRepo.GetByID(ctx, *req.GroupID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("group not found") + } + return nil, fmt.Errorf("get group: %w", err) + } + + if !s.canUserBindGroup(ctx, user, group) { + return nil, ErrGroupNotAllowed + } + + apiKey.GroupID = req.GroupID + } + + if req.Status != nil { + apiKey.Status = *req.Status + // 如果状态改变,清除Redis缓存 + if s.rdb != nil { + cacheKey := fmt.Sprintf("apikey:%s", apiKey.Key) + _ = s.rdb.Del(ctx, cacheKey) + } + } + + if err := s.apiKeyRepo.Update(ctx, apiKey); err != nil { + return nil, fmt.Errorf("update api key: %w", err) + } + + return apiKey, nil +} + +// Delete 删除API Key +func (s *ApiKeyService) Delete(ctx context.Context, id int64, userID int64) error { + apiKey, err := s.apiKeyRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrApiKeyNotFound + } + return fmt.Errorf("get api key: %w", err) + } + + // 验证所有权 + if apiKey.UserID != userID { + return ErrInsufficientPerms + } + + // 清除Redis缓存 + if s.rdb != nil { + cacheKey := fmt.Sprintf("apikey:%s", apiKey.Key) + _ = s.rdb.Del(ctx, cacheKey) + } + + if err := s.apiKeyRepo.Delete(ctx, id); err != nil { + return fmt.Errorf("delete api key: %w", err) + } + + return nil +} + +// ValidateKey 验证API Key是否有效(用于认证中间件) +func (s *ApiKeyService) ValidateKey(ctx context.Context, key string) (*model.ApiKey, *model.User, error) { + // 获取API Key + apiKey, err := s.GetByKey(ctx, key) + if err != nil { + return nil, nil, err + } + + // 检查API Key状态 + if !apiKey.IsActive() { + return nil, nil, errors.New("api key is not active") + } + + // 获取用户信息 + user, err := s.userRepo.GetByID(ctx, apiKey.UserID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil, ErrUserNotFound + } + return nil, nil, fmt.Errorf("get user: %w", err) + } + + // 检查用户状态 + if !user.IsActive() { + return nil, nil, ErrUserNotActive + } + + return apiKey, user, nil +} + +// IncrementUsage 增加API Key使用次数(可选:用于统计) +func (s *ApiKeyService) IncrementUsage(ctx context.Context, keyID int64) error { + // 使用Redis计数器 + if s.rdb != nil { + cacheKey := fmt.Sprintf("apikey:usage:%d:%s", keyID, timezone.Now().Format("2006-01-02")) + if err := s.rdb.Incr(ctx, cacheKey).Err(); err != nil { + return fmt.Errorf("increment usage: %w", err) + } + // 设置24小时过期 + _ = s.rdb.Expire(ctx, cacheKey, 24*time.Hour) + } + return nil +} + +// GetAvailableGroups 获取用户有权限绑定的分组列表 +// 返回用户可以选择的分组: +// - 标准类型分组:公开的(非专属)或用户被明确允许的 +// - 订阅类型分组:用户有有效订阅的 +func (s *ApiKeyService) GetAvailableGroups(ctx context.Context, userID int64) ([]model.Group, error) { + // 获取用户信息 + user, err := s.userRepo.GetByID(ctx, userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrUserNotFound + } + return nil, fmt.Errorf("get user: %w", err) + } + + // 获取所有活跃分组 + allGroups, err := s.groupRepo.ListActive(ctx) + if err != nil { + return nil, fmt.Errorf("list active groups: %w", err) + } + + // 获取用户的所有有效订阅 + activeSubscriptions, err := s.userSubRepo.ListActiveByUserID(ctx, userID) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("list active subscriptions: %w", err) + } + + // 构建订阅分组 ID 集合 + subscribedGroupIDs := make(map[int64]bool) + for _, sub := range activeSubscriptions { + subscribedGroupIDs[sub.GroupID] = true + } + + // 过滤出用户有权限的分组 + availableGroups := make([]model.Group, 0) + for _, group := range allGroups { + if s.canUserBindGroupInternal(user, &group, subscribedGroupIDs) { + availableGroups = append(availableGroups, group) + } + } + + return availableGroups, nil +} + +// canUserBindGroupInternal 内部方法,检查用户是否可以绑定分组(使用预加载的订阅数据) +func (s *ApiKeyService) canUserBindGroupInternal(user *model.User, group *model.Group, subscribedGroupIDs map[int64]bool) bool { + // 订阅类型分组:需要有效订阅 + if group.IsSubscriptionType() { + return subscribedGroupIDs[group.ID] + } + // 标准类型分组:使用原有逻辑 + return user.CanBindGroup(group.ID, group.IsExclusive) +} diff --git a/backend/internal/service/auth_service.go b/backend/internal/service/auth_service.go new file mode 100644 index 00000000..2a063bd1 --- /dev/null +++ b/backend/internal/service/auth_service.go @@ -0,0 +1,376 @@ +package service + +import ( + "context" + "errors" + "fmt" + "log" + "sub2api/internal/config" + "sub2api/internal/model" + "sub2api/internal/repository" + "time" + + "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +var ( + ErrInvalidCredentials = errors.New("invalid email or password") + ErrUserNotActive = errors.New("user is not active") + ErrEmailExists = errors.New("email already exists") + ErrInvalidToken = errors.New("invalid token") + ErrTokenExpired = errors.New("token has expired") + ErrEmailVerifyRequired = errors.New("email verification is required") + ErrRegDisabled = errors.New("registration is currently disabled") +) + +// JWTClaims JWT载荷数据 +type JWTClaims struct { + UserID int64 `json:"user_id"` + Email string `json:"email"` + Role string `json:"role"` + jwt.RegisteredClaims +} + +// AuthService 认证服务 +type AuthService struct { + userRepo *repository.UserRepository + cfg *config.Config + settingService *SettingService + emailService *EmailService + turnstileService *TurnstileService + emailQueueService *EmailQueueService +} + +// NewAuthService 创建认证服务实例 +func NewAuthService(userRepo *repository.UserRepository, cfg *config.Config) *AuthService { + return &AuthService{ + userRepo: userRepo, + cfg: cfg, + } +} + +// SetSettingService 设置系统设置服务(用于检查注册开关和邮件验证) +func (s *AuthService) SetSettingService(settingService *SettingService) { + s.settingService = settingService +} + +// SetEmailService 设置邮件服务(用于邮件验证) +func (s *AuthService) SetEmailService(emailService *EmailService) { + s.emailService = emailService +} + +// SetTurnstileService 设置Turnstile服务(用于验证码校验) +func (s *AuthService) SetTurnstileService(turnstileService *TurnstileService) { + s.turnstileService = turnstileService +} + +// SetEmailQueueService 设置邮件队列服务(用于异步发送邮件) +func (s *AuthService) SetEmailQueueService(emailQueueService *EmailQueueService) { + s.emailQueueService = emailQueueService +} + +// Register 用户注册,返回token和用户 +func (s *AuthService) Register(ctx context.Context, email, password string) (string, *model.User, error) { + return s.RegisterWithVerification(ctx, email, password, "") +} + +// RegisterWithVerification 用户注册(支持邮件验证),返回token和用户 +func (s *AuthService) RegisterWithVerification(ctx context.Context, email, password, verifyCode string) (string, *model.User, error) { + // 检查是否开放注册 + if s.settingService != nil && !s.settingService.IsRegistrationEnabled(ctx) { + return "", nil, ErrRegDisabled + } + + // 检查是否需要邮件验证 + if s.settingService != nil && s.settingService.IsEmailVerifyEnabled(ctx) { + if verifyCode == "" { + return "", nil, ErrEmailVerifyRequired + } + // 验证邮箱验证码 + if s.emailService != nil { + if err := s.emailService.VerifyCode(ctx, email, verifyCode); err != nil { + return "", nil, fmt.Errorf("verify code: %w", err) + } + } + } + + // 检查邮箱是否已存在 + existsEmail, err := s.userRepo.ExistsByEmail(ctx, email) + if err != nil { + return "", nil, fmt.Errorf("check email exists: %w", err) + } + if existsEmail { + return "", nil, ErrEmailExists + } + + // 密码哈希 + hashedPassword, err := s.HashPassword(password) + if err != nil { + return "", nil, fmt.Errorf("hash password: %w", err) + } + + // 获取默认配置 + defaultBalance := s.cfg.Default.UserBalance + defaultConcurrency := s.cfg.Default.UserConcurrency + if s.settingService != nil { + defaultBalance = s.settingService.GetDefaultBalance(ctx) + defaultConcurrency = s.settingService.GetDefaultConcurrency(ctx) + } + + // 创建用户 + user := &model.User{ + Email: email, + PasswordHash: hashedPassword, + Role: model.RoleUser, + Balance: defaultBalance, + Concurrency: defaultConcurrency, + Status: model.StatusActive, + } + + if err := s.userRepo.Create(ctx, user); err != nil { + return "", nil, fmt.Errorf("create user: %w", err) + } + + // 生成token + token, err := s.GenerateToken(user) + if err != nil { + return "", nil, fmt.Errorf("generate token: %w", err) + } + + return token, user, nil +} + +// SendVerifyCodeResult 发送验证码返回结果 +type SendVerifyCodeResult struct { + Countdown int `json:"countdown"` // 倒计时秒数 +} + +// SendVerifyCode 发送邮箱验证码(同步方式) +func (s *AuthService) SendVerifyCode(ctx context.Context, email string) error { + // 检查是否开放注册 + if s.settingService != nil && !s.settingService.IsRegistrationEnabled(ctx) { + return ErrRegDisabled + } + + // 检查邮箱是否已存在 + existsEmail, err := s.userRepo.ExistsByEmail(ctx, email) + if err != nil { + return fmt.Errorf("check email exists: %w", err) + } + if existsEmail { + return ErrEmailExists + } + + // 发送验证码 + if s.emailService == nil { + return errors.New("email service not configured") + } + + // 获取网站名称 + siteName := "Sub2API" + if s.settingService != nil { + siteName = s.settingService.GetSiteName(ctx) + } + + return s.emailService.SendVerifyCode(ctx, email, siteName) +} + +// SendVerifyCodeAsync 异步发送邮箱验证码并返回倒计时 +func (s *AuthService) SendVerifyCodeAsync(ctx context.Context, email string) (*SendVerifyCodeResult, error) { + log.Printf("[Auth] SendVerifyCodeAsync called for email: %s", email) + + // 检查是否开放注册 + if s.settingService != nil && !s.settingService.IsRegistrationEnabled(ctx) { + log.Println("[Auth] Registration is disabled") + return nil, ErrRegDisabled + } + + // 检查邮箱是否已存在 + existsEmail, err := s.userRepo.ExistsByEmail(ctx, email) + if err != nil { + log.Printf("[Auth] Error checking email exists: %v", err) + return nil, fmt.Errorf("check email exists: %w", err) + } + if existsEmail { + log.Printf("[Auth] Email already exists: %s", email) + return nil, ErrEmailExists + } + + // 检查邮件队列服务是否配置 + if s.emailQueueService == nil { + log.Println("[Auth] Email queue service not configured") + return nil, errors.New("email queue service not configured") + } + + // 获取网站名称 + siteName := "Sub2API" + if s.settingService != nil { + siteName = s.settingService.GetSiteName(ctx) + } + + // 异步发送 + log.Printf("[Auth] Enqueueing verify code for: %s", email) + if err := s.emailQueueService.EnqueueVerifyCode(email, siteName); err != nil { + log.Printf("[Auth] Failed to enqueue: %v", err) + return nil, fmt.Errorf("enqueue verify code: %w", err) + } + + log.Printf("[Auth] Verify code enqueued successfully for: %s", email) + return &SendVerifyCodeResult{ + Countdown: 60, // 60秒倒计时 + }, nil +} + +// VerifyTurnstile 验证Turnstile token +func (s *AuthService) VerifyTurnstile(ctx context.Context, token string, remoteIP string) error { + if s.turnstileService == nil { + return nil // 服务未配置则跳过验证 + } + return s.turnstileService.VerifyToken(ctx, token, remoteIP) +} + +// IsTurnstileEnabled 检查是否启用Turnstile验证 +func (s *AuthService) IsTurnstileEnabled(ctx context.Context) bool { + if s.turnstileService == nil { + return false + } + return s.turnstileService.IsEnabled(ctx) +} + +// IsRegistrationEnabled 检查是否开放注册 +func (s *AuthService) IsRegistrationEnabled(ctx context.Context) bool { + if s.settingService == nil { + return true + } + return s.settingService.IsRegistrationEnabled(ctx) +} + +// IsEmailVerifyEnabled 检查是否开启邮件验证 +func (s *AuthService) IsEmailVerifyEnabled(ctx context.Context) bool { + if s.settingService == nil { + return false + } + return s.settingService.IsEmailVerifyEnabled(ctx) +} + +// Login 用户登录,返回JWT token +func (s *AuthService) Login(ctx context.Context, email, password string) (string, *model.User, error) { + // 查找用户 + user, err := s.userRepo.GetByEmail(ctx, email) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return "", nil, ErrInvalidCredentials + } + return "", nil, fmt.Errorf("get user by email: %w", err) + } + + // 验证密码 + if !s.CheckPassword(password, user.PasswordHash) { + return "", nil, ErrInvalidCredentials + } + + // 检查用户状态 + if !user.IsActive() { + return "", nil, ErrUserNotActive + } + + // 生成JWT token + token, err := s.GenerateToken(user) + if err != nil { + return "", nil, fmt.Errorf("generate token: %w", err) + } + + return token, user, nil +} + +// ValidateToken 验证JWT token并返回用户声明 +func (s *AuthService) ValidateToken(tokenString string) (*JWTClaims, error) { + token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) { + // 验证签名方法 + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(s.cfg.JWT.Secret), nil + }) + + if err != nil { + if errors.Is(err, jwt.ErrTokenExpired) { + return nil, ErrTokenExpired + } + return nil, ErrInvalidToken + } + + if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid { + return claims, nil + } + + return nil, ErrInvalidToken +} + +// GenerateToken 生成JWT token +func (s *AuthService) GenerateToken(user *model.User) (string, error) { + now := time.Now() + expiresAt := now.Add(time.Duration(s.cfg.JWT.ExpireHour) * time.Hour) + + claims := &JWTClaims{ + UserID: user.ID, + Email: user.Email, + Role: user.Role, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expiresAt), + IssuedAt: jwt.NewNumericDate(now), + NotBefore: jwt.NewNumericDate(now), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(s.cfg.JWT.Secret)) + if err != nil { + return "", fmt.Errorf("sign token: %w", err) + } + + return tokenString, nil +} + +// HashPassword 使用bcrypt加密密码 +func (s *AuthService) HashPassword(password string) (string, error) { + hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(hashedBytes), nil +} + +// CheckPassword 验证密码是否匹配 +func (s *AuthService) CheckPassword(password, hashedPassword string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) + return err == nil +} + +// RefreshToken 刷新token +func (s *AuthService) RefreshToken(ctx context.Context, oldTokenString string) (string, error) { + // 验证旧token(即使过期也允许,用于刷新) + claims, err := s.ValidateToken(oldTokenString) + if err != nil && !errors.Is(err, ErrTokenExpired) { + return "", err + } + + // 获取最新的用户信息 + user, err := s.userRepo.GetByID(ctx, claims.UserID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return "", ErrInvalidToken + } + return "", fmt.Errorf("get user: %w", err) + } + + // 检查用户状态 + if !user.IsActive() { + return "", ErrUserNotActive + } + + // 生成新token + return s.GenerateToken(user) +} diff --git a/backend/internal/service/billing_cache_service.go b/backend/internal/service/billing_cache_service.go new file mode 100644 index 00000000..0921a153 --- /dev/null +++ b/backend/internal/service/billing_cache_service.go @@ -0,0 +1,422 @@ +package service + +import ( + "context" + "errors" + "fmt" + "log" + "strconv" + "time" + + "sub2api/internal/model" + "sub2api/internal/repository" + + "github.com/redis/go-redis/v9" +) + +// 缓存Key前缀和TTL +const ( + billingBalanceKeyPrefix = "billing:balance:" + billingSubKeyPrefix = "billing:sub:" + billingCacheTTL = 5 * time.Minute +) + +// 订阅缓存Hash字段 +const ( + subFieldStatus = "status" + subFieldExpiresAt = "expires_at" + subFieldDailyUsage = "daily_usage" + subFieldWeeklyUsage = "weekly_usage" + subFieldMonthlyUsage = "monthly_usage" + subFieldVersion = "version" +) + +// 错误定义 +// 注:ErrInsufficientBalance在redeem_service.go中定义 +// 注:ErrDailyLimitExceeded/ErrWeeklyLimitExceeded/ErrMonthlyLimitExceeded在subscription_service.go中定义 +var ( + ErrSubscriptionInvalid = errors.New("subscription is invalid or expired") +) + +// 预编译的Lua脚本 +var ( + // deductBalanceScript: 扣减余额缓存,key不存在则忽略 + deductBalanceScript = redis.NewScript(` + local current = redis.call('GET', KEYS[1]) + if current == false then + return 0 + end + local newVal = tonumber(current) - tonumber(ARGV[1]) + redis.call('SET', KEYS[1], newVal) + redis.call('EXPIRE', KEYS[1], ARGV[2]) + return 1 + `) + + // updateSubUsageScript: 更新订阅用量缓存,key不存在则忽略 + updateSubUsageScript = redis.NewScript(` + local exists = redis.call('EXISTS', KEYS[1]) + if exists == 0 then + return 0 + end + local cost = tonumber(ARGV[1]) + redis.call('HINCRBYFLOAT', KEYS[1], 'daily_usage', cost) + redis.call('HINCRBYFLOAT', KEYS[1], 'weekly_usage', cost) + redis.call('HINCRBYFLOAT', KEYS[1], 'monthly_usage', cost) + redis.call('EXPIRE', KEYS[1], ARGV[2]) + return 1 + `) +) + +// subscriptionCacheData 订阅缓存数据结构(内部使用) +type subscriptionCacheData struct { + Status string + ExpiresAt time.Time + DailyUsage float64 + WeeklyUsage float64 + MonthlyUsage float64 + Version int64 +} + +// BillingCacheService 计费缓存服务 +// 负责余额和订阅数据的缓存管理,提供高性能的计费资格检查 +type BillingCacheService struct { + rdb *redis.Client + userRepo *repository.UserRepository + subRepo *repository.UserSubscriptionRepository +} + +// NewBillingCacheService 创建计费缓存服务 +func NewBillingCacheService(rdb *redis.Client, userRepo *repository.UserRepository, subRepo *repository.UserSubscriptionRepository) *BillingCacheService { + return &BillingCacheService{ + rdb: rdb, + userRepo: userRepo, + subRepo: subRepo, + } +} + +// ============================================ +// 余额缓存方法 +// ============================================ + +// GetUserBalance 获取用户余额(优先从缓存读取) +func (s *BillingCacheService) GetUserBalance(ctx context.Context, userID int64) (float64, error) { + if s.rdb == nil { + // Redis不可用,直接查询数据库 + return s.getUserBalanceFromDB(ctx, userID) + } + + key := fmt.Sprintf("%s%d", billingBalanceKeyPrefix, userID) + + // 尝试从缓存读取 + val, err := s.rdb.Get(ctx, key).Result() + if err == nil { + balance, parseErr := strconv.ParseFloat(val, 64) + if parseErr == nil { + return balance, nil + } + } + + // 缓存未命中或解析错误,从数据库读取 + balance, err := s.getUserBalanceFromDB(ctx, userID) + if err != nil { + return 0, err + } + + // 异步建立缓存 + go func() { + cacheCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + s.setBalanceCache(cacheCtx, userID, balance) + }() + + return balance, nil +} + +// getUserBalanceFromDB 从数据库获取用户余额 +func (s *BillingCacheService) getUserBalanceFromDB(ctx context.Context, userID int64) (float64, error) { + user, err := s.userRepo.GetByID(ctx, userID) + if err != nil { + return 0, fmt.Errorf("get user balance: %w", err) + } + return user.Balance, nil +} + +// setBalanceCache 设置余额缓存 +func (s *BillingCacheService) setBalanceCache(ctx context.Context, userID int64, balance float64) { + if s.rdb == nil { + return + } + key := fmt.Sprintf("%s%d", billingBalanceKeyPrefix, userID) + if err := s.rdb.Set(ctx, key, balance, billingCacheTTL).Err(); err != nil { + log.Printf("Warning: set balance cache failed for user %d: %v", userID, err) + } +} + +// DeductBalanceCache 扣减余额缓存(异步调用,用于扣费后更新缓存) +func (s *BillingCacheService) DeductBalanceCache(ctx context.Context, userID int64, amount float64) error { + if s.rdb == nil { + return nil + } + + key := fmt.Sprintf("%s%d", billingBalanceKeyPrefix, userID) + + // 使用预编译的Lua脚本原子性扣减,如果key不存在则忽略 + _, err := deductBalanceScript.Run(ctx, s.rdb, []string{key}, amount, int(billingCacheTTL.Seconds())).Result() + if err != nil && !errors.Is(err, redis.Nil) { + log.Printf("Warning: deduct balance cache failed for user %d: %v", userID, err) + } + return nil +} + +// InvalidateUserBalance 失效用户余额缓存 +func (s *BillingCacheService) InvalidateUserBalance(ctx context.Context, userID int64) error { + if s.rdb == nil { + return nil + } + + key := fmt.Sprintf("%s%d", billingBalanceKeyPrefix, userID) + if err := s.rdb.Del(ctx, key).Err(); err != nil { + log.Printf("Warning: invalidate balance cache failed for user %d: %v", userID, err) + return err + } + return nil +} + +// ============================================ +// 订阅缓存方法 +// ============================================ + +// GetSubscriptionStatus 获取订阅状态(优先从缓存读取) +func (s *BillingCacheService) GetSubscriptionStatus(ctx context.Context, userID, groupID int64) (*subscriptionCacheData, error) { + if s.rdb == nil { + return s.getSubscriptionFromDB(ctx, userID, groupID) + } + + key := fmt.Sprintf("%s%d:%d", billingSubKeyPrefix, userID, groupID) + + // 尝试从缓存读取 + result, err := s.rdb.HGetAll(ctx, key).Result() + if err == nil && len(result) > 0 { + data, parseErr := s.parseSubscriptionCache(result) + if parseErr == nil { + return data, nil + } + } + + // 缓存未命中,从数据库读取 + data, err := s.getSubscriptionFromDB(ctx, userID, groupID) + if err != nil { + return nil, err + } + + // 异步建立缓存 + go func() { + cacheCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + s.setSubscriptionCache(cacheCtx, userID, groupID, data) + }() + + return data, nil +} + +// getSubscriptionFromDB 从数据库获取订阅数据 +func (s *BillingCacheService) getSubscriptionFromDB(ctx context.Context, userID, groupID int64) (*subscriptionCacheData, error) { + sub, err := s.subRepo.GetActiveByUserIDAndGroupID(ctx, userID, groupID) + if err != nil { + return nil, fmt.Errorf("get subscription: %w", err) + } + + return &subscriptionCacheData{ + Status: sub.Status, + ExpiresAt: sub.ExpiresAt, + DailyUsage: sub.DailyUsageUSD, + WeeklyUsage: sub.WeeklyUsageUSD, + MonthlyUsage: sub.MonthlyUsageUSD, + Version: sub.UpdatedAt.Unix(), + }, nil +} + +// parseSubscriptionCache 解析订阅缓存数据 +func (s *BillingCacheService) parseSubscriptionCache(data map[string]string) (*subscriptionCacheData, error) { + result := &subscriptionCacheData{} + + result.Status = data[subFieldStatus] + if result.Status == "" { + return nil, errors.New("invalid cache: missing status") + } + + if expiresStr, ok := data[subFieldExpiresAt]; ok { + expiresAt, err := strconv.ParseInt(expiresStr, 10, 64) + if err == nil { + result.ExpiresAt = time.Unix(expiresAt, 0) + } + } + + if dailyStr, ok := data[subFieldDailyUsage]; ok { + result.DailyUsage, _ = strconv.ParseFloat(dailyStr, 64) + } + + if weeklyStr, ok := data[subFieldWeeklyUsage]; ok { + result.WeeklyUsage, _ = strconv.ParseFloat(weeklyStr, 64) + } + + if monthlyStr, ok := data[subFieldMonthlyUsage]; ok { + result.MonthlyUsage, _ = strconv.ParseFloat(monthlyStr, 64) + } + + if versionStr, ok := data[subFieldVersion]; ok { + result.Version, _ = strconv.ParseInt(versionStr, 10, 64) + } + + return result, nil +} + +// setSubscriptionCache 设置订阅缓存 +func (s *BillingCacheService) setSubscriptionCache(ctx context.Context, userID, groupID int64, data *subscriptionCacheData) { + if s.rdb == nil || data == nil { + return + } + + key := fmt.Sprintf("%s%d:%d", billingSubKeyPrefix, userID, groupID) + + fields := map[string]interface{}{ + subFieldStatus: data.Status, + subFieldExpiresAt: data.ExpiresAt.Unix(), + subFieldDailyUsage: data.DailyUsage, + subFieldWeeklyUsage: data.WeeklyUsage, + subFieldMonthlyUsage: data.MonthlyUsage, + subFieldVersion: data.Version, + } + + pipe := s.rdb.Pipeline() + pipe.HSet(ctx, key, fields) + pipe.Expire(ctx, key, billingCacheTTL) + if _, err := pipe.Exec(ctx); err != nil { + log.Printf("Warning: set subscription cache failed for user %d group %d: %v", userID, groupID, err) + } +} + +// UpdateSubscriptionUsage 更新订阅用量缓存(异步调用,用于扣费后更新缓存) +func (s *BillingCacheService) UpdateSubscriptionUsage(ctx context.Context, userID, groupID int64, costUSD float64) error { + if s.rdb == nil { + return nil + } + + key := fmt.Sprintf("%s%d:%d", billingSubKeyPrefix, userID, groupID) + + // 使用预编译的Lua脚本原子性增加用量,如果key不存在则忽略 + _, err := updateSubUsageScript.Run(ctx, s.rdb, []string{key}, costUSD, int(billingCacheTTL.Seconds())).Result() + if err != nil && !errors.Is(err, redis.Nil) { + log.Printf("Warning: update subscription usage cache failed for user %d group %d: %v", userID, groupID, err) + } + return nil +} + +// InvalidateSubscription 失效指定订阅缓存 +func (s *BillingCacheService) InvalidateSubscription(ctx context.Context, userID, groupID int64) error { + if s.rdb == nil { + return nil + } + + key := fmt.Sprintf("%s%d:%d", billingSubKeyPrefix, userID, groupID) + if err := s.rdb.Del(ctx, key).Err(); err != nil { + log.Printf("Warning: invalidate subscription cache failed for user %d group %d: %v", userID, groupID, err) + return err + } + return nil +} + +// ============================================ +// 统一检查方法 +// ============================================ + +// CheckBillingEligibility 检查用户是否有资格发起请求 +// 余额模式:检查缓存余额 > 0 +// 订阅模式:检查缓存用量未超过限额(Group限额从参数传入) +func (s *BillingCacheService) CheckBillingEligibility(ctx context.Context, user *model.User, apiKey *model.ApiKey, group *model.Group, subscription *model.UserSubscription) error { + // 判断计费模式 + isSubscriptionMode := group != nil && group.IsSubscriptionType() && subscription != nil + + if isSubscriptionMode { + return s.checkSubscriptionEligibility(ctx, user.ID, group, subscription) + } + + return s.checkBalanceEligibility(ctx, user.ID) +} + +// checkBalanceEligibility 检查余额模式资格 +func (s *BillingCacheService) checkBalanceEligibility(ctx context.Context, userID int64) error { + balance, err := s.GetUserBalance(ctx, userID) + if err != nil { + // 缓存/数据库错误,允许通过(降级处理) + log.Printf("Warning: get user balance failed, allowing request: %v", err) + return nil + } + + if balance <= 0 { + return ErrInsufficientBalance + } + + return nil +} + +// checkSubscriptionEligibility 检查订阅模式资格 +func (s *BillingCacheService) checkSubscriptionEligibility(ctx context.Context, userID int64, group *model.Group, subscription *model.UserSubscription) error { + // 获取订阅缓存数据 + subData, err := s.GetSubscriptionStatus(ctx, userID, group.ID) + if err != nil { + // 缓存/数据库错误,降级使用传入的subscription进行检查 + log.Printf("Warning: get subscription cache failed, using fallback: %v", err) + return s.checkSubscriptionLimitsFallback(subscription, group) + } + + // 检查订阅状态 + if subData.Status != model.SubscriptionStatusActive { + return ErrSubscriptionInvalid + } + + // 检查是否过期 + if time.Now().After(subData.ExpiresAt) { + return ErrSubscriptionInvalid + } + + // 检查限额(使用传入的Group限额配置) + if group.HasDailyLimit() && subData.DailyUsage >= *group.DailyLimitUSD { + return ErrDailyLimitExceeded + } + + if group.HasWeeklyLimit() && subData.WeeklyUsage >= *group.WeeklyLimitUSD { + return ErrWeeklyLimitExceeded + } + + if group.HasMonthlyLimit() && subData.MonthlyUsage >= *group.MonthlyLimitUSD { + return ErrMonthlyLimitExceeded + } + + return nil +} + +// checkSubscriptionLimitsFallback 降级检查订阅限额 +func (s *BillingCacheService) checkSubscriptionLimitsFallback(subscription *model.UserSubscription, group *model.Group) error { + if subscription == nil { + return ErrSubscriptionInvalid + } + + if !subscription.IsActive() { + return ErrSubscriptionInvalid + } + + if !subscription.CheckDailyLimit(group, 0) { + return ErrDailyLimitExceeded + } + + if !subscription.CheckWeeklyLimit(group, 0) { + return ErrWeeklyLimitExceeded + } + + if !subscription.CheckMonthlyLimit(group, 0) { + return ErrMonthlyLimitExceeded + } + + return nil +} diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go new file mode 100644 index 00000000..e1e4f1bb --- /dev/null +++ b/backend/internal/service/billing_service.go @@ -0,0 +1,279 @@ +package service + +import ( + "fmt" + "log" + "strings" + "sub2api/internal/config" +) + +// ModelPricing 模型价格配置(per-token价格,与LiteLLM格式一致) +type ModelPricing struct { + InputPricePerToken float64 // 每token输入价格 (USD) + OutputPricePerToken float64 // 每token输出价格 (USD) + CacheCreationPricePerToken float64 // 缓存创建每token价格 (USD) + CacheReadPricePerToken float64 // 缓存读取每token价格 (USD) + CacheCreation5mPrice float64 // 5分钟缓存创建价格(每百万token)- 仅用于硬编码回退 + CacheCreation1hPrice float64 // 1小时缓存创建价格(每百万token)- 仅用于硬编码回退 + SupportsCacheBreakdown bool // 是否支持详细的缓存分类 +} + +// UsageTokens 使用的token数量 +type UsageTokens struct { + InputTokens int + OutputTokens int + CacheCreationTokens int + CacheReadTokens int + CacheCreation5mTokens int + CacheCreation1hTokens int +} + +// CostBreakdown 费用明细 +type CostBreakdown struct { + InputCost float64 + OutputCost float64 + CacheCreationCost float64 + CacheReadCost float64 + TotalCost float64 + ActualCost float64 // 应用倍率后的实际费用 +} + +// BillingService 计费服务 +type BillingService struct { + cfg *config.Config + pricingService *PricingService + fallbackPrices map[string]*ModelPricing // 硬编码回退价格 +} + +// NewBillingService 创建计费服务实例 +func NewBillingService(cfg *config.Config, pricingService *PricingService) *BillingService { + s := &BillingService{ + cfg: cfg, + pricingService: pricingService, + fallbackPrices: make(map[string]*ModelPricing), + } + + // 初始化硬编码回退价格(当动态价格不可用时使用) + s.initFallbackPricing() + + return s +} + +// initFallbackPricing 初始化硬编码回退价格(当动态价格不可用时使用) +// 价格单位:USD per token(与LiteLLM格式一致) +func (s *BillingService) initFallbackPricing() { + // Claude 4.5 Opus + s.fallbackPrices["claude-opus-4.5"] = &ModelPricing{ + InputPricePerToken: 5e-6, // $5 per MTok + OutputPricePerToken: 25e-6, // $25 per MTok + CacheCreationPricePerToken: 6.25e-6, // $6.25 per MTok + CacheReadPricePerToken: 0.5e-6, // $0.50 per MTok + SupportsCacheBreakdown: false, + } + + // Claude 4 Sonnet + s.fallbackPrices["claude-sonnet-4"] = &ModelPricing{ + InputPricePerToken: 3e-6, // $3 per MTok + OutputPricePerToken: 15e-6, // $15 per MTok + CacheCreationPricePerToken: 3.75e-6, // $3.75 per MTok + CacheReadPricePerToken: 0.3e-6, // $0.30 per MTok + SupportsCacheBreakdown: false, + } + + // Claude 3.5 Sonnet + s.fallbackPrices["claude-3-5-sonnet"] = &ModelPricing{ + InputPricePerToken: 3e-6, // $3 per MTok + OutputPricePerToken: 15e-6, // $15 per MTok + CacheCreationPricePerToken: 3.75e-6, // $3.75 per MTok + CacheReadPricePerToken: 0.3e-6, // $0.30 per MTok + SupportsCacheBreakdown: false, + } + + // Claude 3.5 Haiku + s.fallbackPrices["claude-3-5-haiku"] = &ModelPricing{ + InputPricePerToken: 1e-6, // $1 per MTok + OutputPricePerToken: 5e-6, // $5 per MTok + CacheCreationPricePerToken: 1.25e-6, // $1.25 per MTok + CacheReadPricePerToken: 0.1e-6, // $0.10 per MTok + SupportsCacheBreakdown: false, + } + + // Claude 3 Opus + s.fallbackPrices["claude-3-opus"] = &ModelPricing{ + InputPricePerToken: 15e-6, // $15 per MTok + OutputPricePerToken: 75e-6, // $75 per MTok + CacheCreationPricePerToken: 18.75e-6, // $18.75 per MTok + CacheReadPricePerToken: 1.5e-6, // $1.50 per MTok + SupportsCacheBreakdown: false, + } + + // Claude 3 Haiku + s.fallbackPrices["claude-3-haiku"] = &ModelPricing{ + InputPricePerToken: 0.25e-6, // $0.25 per MTok + OutputPricePerToken: 1.25e-6, // $1.25 per MTok + CacheCreationPricePerToken: 0.3e-6, // $0.30 per MTok + CacheReadPricePerToken: 0.03e-6, // $0.03 per MTok + SupportsCacheBreakdown: false, + } +} + +// getFallbackPricing 根据模型系列获取回退价格 +func (s *BillingService) getFallbackPricing(model string) *ModelPricing { + modelLower := strings.ToLower(model) + + // 按模型系列匹配 + if strings.Contains(modelLower, "opus") { + if strings.Contains(modelLower, "4.5") || strings.Contains(modelLower, "4-5") { + return s.fallbackPrices["claude-opus-4.5"] + } + return s.fallbackPrices["claude-3-opus"] + } + if strings.Contains(modelLower, "sonnet") { + if strings.Contains(modelLower, "4") && !strings.Contains(modelLower, "3") { + return s.fallbackPrices["claude-sonnet-4"] + } + return s.fallbackPrices["claude-3-5-sonnet"] + } + if strings.Contains(modelLower, "haiku") { + if strings.Contains(modelLower, "3-5") || strings.Contains(modelLower, "3.5") { + return s.fallbackPrices["claude-3-5-haiku"] + } + return s.fallbackPrices["claude-3-haiku"] + } + + // 默认使用Sonnet价格 + return s.fallbackPrices["claude-sonnet-4"] +} + +// GetModelPricing 获取模型价格配置 +func (s *BillingService) GetModelPricing(model string) (*ModelPricing, error) { + // 标准化模型名称(转小写) + model = strings.ToLower(model) + + // 1. 优先从动态价格服务获取 + if s.pricingService != nil { + litellmPricing := s.pricingService.GetModelPricing(model) + if litellmPricing != nil { + return &ModelPricing{ + InputPricePerToken: litellmPricing.InputCostPerToken, + OutputPricePerToken: litellmPricing.OutputCostPerToken, + CacheCreationPricePerToken: litellmPricing.CacheCreationInputTokenCost, + CacheReadPricePerToken: litellmPricing.CacheReadInputTokenCost, + SupportsCacheBreakdown: false, + }, nil + } + } + + // 2. 使用硬编码回退价格 + fallback := s.getFallbackPricing(model) + if fallback != nil { + log.Printf("[Billing] Using fallback pricing for model: %s", model) + return fallback, nil + } + + return nil, fmt.Errorf("pricing not found for model: %s", model) +} + +// CalculateCost 计算使用费用 +func (s *BillingService) CalculateCost(model string, tokens UsageTokens, rateMultiplier float64) (*CostBreakdown, error) { + pricing, err := s.GetModelPricing(model) + if err != nil { + return nil, err + } + + breakdown := &CostBreakdown{} + + // 计算输入token费用(使用per-token价格) + breakdown.InputCost = float64(tokens.InputTokens) * pricing.InputPricePerToken + + // 计算输出token费用 + breakdown.OutputCost = float64(tokens.OutputTokens) * pricing.OutputPricePerToken + + // 计算缓存费用 + if pricing.SupportsCacheBreakdown && (pricing.CacheCreation5mPrice > 0 || pricing.CacheCreation1hPrice > 0) { + // 支持详细缓存分类的模型(5分钟/1小时缓存) + breakdown.CacheCreationCost = float64(tokens.CacheCreation5mTokens)/1_000_000*pricing.CacheCreation5mPrice + + float64(tokens.CacheCreation1hTokens)/1_000_000*pricing.CacheCreation1hPrice + } else { + // 标准缓存创建价格(per-token) + breakdown.CacheCreationCost = float64(tokens.CacheCreationTokens) * pricing.CacheCreationPricePerToken + } + + breakdown.CacheReadCost = float64(tokens.CacheReadTokens) * pricing.CacheReadPricePerToken + + // 计算总费用 + breakdown.TotalCost = breakdown.InputCost + breakdown.OutputCost + + breakdown.CacheCreationCost + breakdown.CacheReadCost + + // 应用倍率计算实际费用 + if rateMultiplier <= 0 { + rateMultiplier = 1.0 + } + breakdown.ActualCost = breakdown.TotalCost * rateMultiplier + + return breakdown, nil +} + +// CalculateCostWithConfig 使用配置中的默认倍率计算费用 +func (s *BillingService) CalculateCostWithConfig(model string, tokens UsageTokens) (*CostBreakdown, error) { + multiplier := s.cfg.Default.RateMultiplier + if multiplier <= 0 { + multiplier = 1.0 + } + return s.CalculateCost(model, tokens, multiplier) +} + +// ListSupportedModels 列出所有支持的模型(现在总是返回true,因为有模糊匹配) +func (s *BillingService) ListSupportedModels() []string { + models := make([]string, 0) + // 返回回退价格支持的模型系列 + for model := range s.fallbackPrices { + models = append(models, model) + } + return models +} + +// IsModelSupported 检查模型是否支持(现在总是返回true,因为有模糊匹配回退) +func (s *BillingService) IsModelSupported(model string) bool { + // 所有Claude模型都有回退价格支持 + modelLower := strings.ToLower(model) + return strings.Contains(modelLower, "claude") || + strings.Contains(modelLower, "opus") || + strings.Contains(modelLower, "sonnet") || + strings.Contains(modelLower, "haiku") +} + +// GetEstimatedCost 估算费用(用于前端展示) +func (s *BillingService) GetEstimatedCost(model string, estimatedInputTokens, estimatedOutputTokens int) (float64, error) { + tokens := UsageTokens{ + InputTokens: estimatedInputTokens, + OutputTokens: estimatedOutputTokens, + } + + breakdown, err := s.CalculateCostWithConfig(model, tokens) + if err != nil { + return 0, err + } + + return breakdown.ActualCost, nil +} + +// GetPricingServiceStatus 获取价格服务状态 +func (s *BillingService) GetPricingServiceStatus() map[string]interface{} { + if s.pricingService != nil { + return s.pricingService.GetStatus() + } + return map[string]interface{}{ + "model_count": len(s.fallbackPrices), + "last_updated": "using fallback", + "local_hash": "N/A", + } +} + +// ForceUpdatePricing 强制更新价格数据 +func (s *BillingService) ForceUpdatePricing() error { + if s.pricingService != nil { + return s.pricingService.ForceUpdate() + } + return fmt.Errorf("pricing service not initialized") +} diff --git a/backend/internal/service/concurrency_service.go b/backend/internal/service/concurrency_service.go new file mode 100644 index 00000000..d03a34f2 --- /dev/null +++ b/backend/internal/service/concurrency_service.go @@ -0,0 +1,251 @@ +package service + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/redis/go-redis/v9" +) + +const ( + // Redis key prefixes + accountConcurrencyKey = "concurrency:account:" + userConcurrencyKey = "concurrency:user:" + userWaitCountKey = "concurrency:wait:" + + // TTL for concurrency keys (auto-release safety net) + concurrencyKeyTTL = 10 * time.Minute + + // Wait polling interval + waitPollInterval = 100 * time.Millisecond + + // Default max wait time + defaultMaxWait = 60 * time.Second + + // Default extra wait slots beyond concurrency limit + defaultExtraWaitSlots = 20 +) + +// Pre-compiled Lua scripts for better performance +var ( + // acquireScript: increment counter if below max, return 1 if successful + acquireScript = redis.NewScript(` + local current = redis.call('GET', KEYS[1]) + if current == false then + current = 0 + else + current = tonumber(current) + end + if current < tonumber(ARGV[1]) then + redis.call('INCR', KEYS[1]) + redis.call('EXPIRE', KEYS[1], ARGV[2]) + return 1 + end + return 0 + `) + + // releaseScript: decrement counter, but don't go below 0 + releaseScript = redis.NewScript(` + local current = redis.call('GET', KEYS[1]) + if current ~= false and tonumber(current) > 0 then + redis.call('DECR', KEYS[1]) + end + return 1 + `) + + // incrementWaitScript: increment wait counter if below max, return 1 if successful + incrementWaitScript = redis.NewScript(` + local waitKey = KEYS[1] + local maxWait = tonumber(ARGV[1]) + local ttl = tonumber(ARGV[2]) + local current = redis.call('GET', waitKey) + if current == false then + current = 0 + else + current = tonumber(current) + end + if current >= maxWait then + return 0 + end + redis.call('INCR', waitKey) + redis.call('EXPIRE', waitKey, ttl) + return 1 + `) + + // decrementWaitScript: decrement wait counter, but don't go below 0 + decrementWaitScript = redis.NewScript(` + local current = redis.call('GET', KEYS[1]) + if current ~= false and tonumber(current) > 0 then + redis.call('DECR', KEYS[1]) + end + return 1 + `) +) + +// ConcurrencyService manages concurrent request limiting for accounts and users +type ConcurrencyService struct { + rdb *redis.Client +} + +// NewConcurrencyService creates a new ConcurrencyService +func NewConcurrencyService(rdb *redis.Client) *ConcurrencyService { + return &ConcurrencyService{rdb: rdb} +} + +// AcquireResult represents the result of acquiring a concurrency slot +type AcquireResult struct { + Acquired bool + ReleaseFunc func() // Must be called when done (typically via defer) +} + +// AcquireAccountSlot attempts to acquire a concurrency slot for an account. +// If the account is at max concurrency, it waits until a slot is available or timeout. +// Returns a release function that MUST be called when the request completes. +func (s *ConcurrencyService) AcquireAccountSlot(ctx context.Context, accountID int64, maxConcurrency int) (*AcquireResult, error) { + key := fmt.Sprintf("%s%d", accountConcurrencyKey, accountID) + return s.acquireSlot(ctx, key, maxConcurrency) +} + +// AcquireUserSlot attempts to acquire a concurrency slot for a user. +// If the user is at max concurrency, it waits until a slot is available or timeout. +// Returns a release function that MUST be called when the request completes. +func (s *ConcurrencyService) AcquireUserSlot(ctx context.Context, userID int64, maxConcurrency int) (*AcquireResult, error) { + key := fmt.Sprintf("%s%d", userConcurrencyKey, userID) + return s.acquireSlot(ctx, key, maxConcurrency) +} + +// acquireSlot is the core implementation for acquiring a concurrency slot +func (s *ConcurrencyService) acquireSlot(ctx context.Context, key string, maxConcurrency int) (*AcquireResult, error) { + // If maxConcurrency is 0 or negative, no limit + if maxConcurrency <= 0 { + return &AcquireResult{ + Acquired: true, + ReleaseFunc: func() {}, // no-op + }, nil + } + + // Try to acquire immediately + acquired, err := s.tryAcquire(ctx, key, maxConcurrency) + if err != nil { + return nil, err + } + + if acquired { + return &AcquireResult{ + Acquired: true, + ReleaseFunc: s.makeReleaseFunc(key), + }, nil + } + + // Not acquired, return with Acquired=false + // The caller (gateway handler) will handle waiting with ping support + return &AcquireResult{ + Acquired: false, + ReleaseFunc: nil, + }, nil +} + +// tryAcquire attempts to increment the counter if below max +// Uses pre-compiled Lua script for atomicity and performance +func (s *ConcurrencyService) tryAcquire(ctx context.Context, key string, maxConcurrency int) (bool, error) { + result, err := acquireScript.Run(ctx, s.rdb, []string{key}, maxConcurrency, int(concurrencyKeyTTL.Seconds())).Int() + if err != nil { + return false, fmt.Errorf("acquire slot failed: %w", err) + } + return result == 1, nil +} + +// makeReleaseFunc creates a function to release a concurrency slot +func (s *ConcurrencyService) makeReleaseFunc(key string) func() { + return func() { + // Use background context to ensure release even if original context is cancelled + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := releaseScript.Run(ctx, s.rdb, []string{key}).Err(); err != nil { + // Log error but don't panic - TTL will eventually clean up + log.Printf("Warning: failed to release concurrency slot for %s: %v", key, err) + } + } +} + +// GetCurrentCount returns the current concurrency count for debugging/monitoring +func (s *ConcurrencyService) GetCurrentCount(ctx context.Context, key string) (int, error) { + val, err := s.rdb.Get(ctx, key).Int() + if err == redis.Nil { + return 0, nil + } + if err != nil { + return 0, err + } + return val, nil +} + +// GetAccountCurrentCount returns current concurrency count for an account +func (s *ConcurrencyService) GetAccountCurrentCount(ctx context.Context, accountID int64) (int, error) { + key := fmt.Sprintf("%s%d", accountConcurrencyKey, accountID) + return s.GetCurrentCount(ctx, key) +} + +// GetUserCurrentCount returns current concurrency count for a user +func (s *ConcurrencyService) GetUserCurrentCount(ctx context.Context, userID int64) (int, error) { + key := fmt.Sprintf("%s%d", userConcurrencyKey, userID) + return s.GetCurrentCount(ctx, key) +} + +// ============================================ +// Wait Queue Count Methods +// ============================================ + +// IncrementWaitCount attempts to increment the wait queue counter for a user. +// Returns true if successful, false if the wait queue is full. +// maxWait should be user.Concurrency + defaultExtraWaitSlots +func (s *ConcurrencyService) IncrementWaitCount(ctx context.Context, userID int64, maxWait int) (bool, error) { + if s.rdb == nil { + // Redis not available, allow request + return true, nil + } + + key := fmt.Sprintf("%s%d", userWaitCountKey, userID) + result, err := incrementWaitScript.Run(ctx, s.rdb, []string{key}, maxWait, int(concurrencyKeyTTL.Seconds())).Int() + if err != nil { + // On error, allow the request to proceed (fail open) + log.Printf("Warning: increment wait count failed for user %d: %v", userID, err) + return true, nil + } + return result == 1, nil +} + +// DecrementWaitCount decrements the wait queue counter for a user. +// Should be called when a request completes or exits the wait queue. +func (s *ConcurrencyService) DecrementWaitCount(ctx context.Context, userID int64) { + if s.rdb == nil { + return + } + + key := fmt.Sprintf("%s%d", userWaitCountKey, userID) + // Use background context to ensure decrement even if original context is cancelled + bgCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := decrementWaitScript.Run(bgCtx, s.rdb, []string{key}).Err(); err != nil { + log.Printf("Warning: decrement wait count failed for user %d: %v", userID, err) + } +} + +// GetUserWaitCount returns current wait queue count for a user +func (s *ConcurrencyService) GetUserWaitCount(ctx context.Context, userID int64) (int, error) { + key := fmt.Sprintf("%s%d", userWaitCountKey, userID) + return s.GetCurrentCount(ctx, key) +} + +// CalculateMaxWait calculates the maximum wait queue size for a user +// maxWait = userConcurrency + defaultExtraWaitSlots +func CalculateMaxWait(userConcurrency int) int { + if userConcurrency <= 0 { + userConcurrency = 1 + } + return userConcurrency + defaultExtraWaitSlots +} diff --git a/backend/internal/service/email_queue_service.go b/backend/internal/service/email_queue_service.go new file mode 100644 index 00000000..1c22702c --- /dev/null +++ b/backend/internal/service/email_queue_service.go @@ -0,0 +1,109 @@ +package service + +import ( + "context" + "fmt" + "log" + "sync" + "time" +) + +// EmailTask 邮件发送任务 +type EmailTask struct { + Email string + SiteName string + TaskType string // "verify_code" +} + +// EmailQueueService 异步邮件队列服务 +type EmailQueueService struct { + emailService *EmailService + taskChan chan EmailTask + wg sync.WaitGroup + stopChan chan struct{} + workers int +} + +// NewEmailQueueService 创建邮件队列服务 +func NewEmailQueueService(emailService *EmailService, workers int) *EmailQueueService { + if workers <= 0 { + workers = 3 // 默认3个工作协程 + } + + service := &EmailQueueService{ + emailService: emailService, + taskChan: make(chan EmailTask, 100), // 缓冲100个任务 + stopChan: make(chan struct{}), + workers: workers, + } + + // 启动工作协程 + service.start() + + return service +} + +// start 启动工作协程 +func (s *EmailQueueService) start() { + for i := 0; i < s.workers; i++ { + s.wg.Add(1) + go s.worker(i) + } + log.Printf("[EmailQueue] Started %d workers", s.workers) +} + +// worker 工作协程 +func (s *EmailQueueService) worker(id int) { + defer s.wg.Done() + + for { + select { + case task := <-s.taskChan: + s.processTask(id, task) + case <-s.stopChan: + log.Printf("[EmailQueue] Worker %d stopping", id) + return + } + } +} + +// processTask 处理任务 +func (s *EmailQueueService) processTask(workerID int, task EmailTask) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + switch task.TaskType { + case "verify_code": + if err := s.emailService.SendVerifyCode(ctx, task.Email, task.SiteName); err != nil { + log.Printf("[EmailQueue] Worker %d failed to send verify code to %s: %v", workerID, task.Email, err) + } else { + log.Printf("[EmailQueue] Worker %d sent verify code to %s", workerID, task.Email) + } + default: + log.Printf("[EmailQueue] Worker %d unknown task type: %s", workerID, task.TaskType) + } +} + +// EnqueueVerifyCode 将验证码发送任务加入队列 +func (s *EmailQueueService) EnqueueVerifyCode(email, siteName string) error { + task := EmailTask{ + Email: email, + SiteName: siteName, + TaskType: "verify_code", + } + + select { + case s.taskChan <- task: + log.Printf("[EmailQueue] Enqueued verify code task for %s", email) + return nil + default: + return fmt.Errorf("email queue is full") + } +} + +// Stop 停止队列服务 +func (s *EmailQueueService) Stop() { + close(s.stopChan) + s.wg.Wait() + log.Println("[EmailQueue] All workers stopped") +} diff --git a/backend/internal/service/email_service.go b/backend/internal/service/email_service.go new file mode 100644 index 00000000..c1fa69f0 --- /dev/null +++ b/backend/internal/service/email_service.go @@ -0,0 +1,372 @@ +package service + +import ( + "context" + "crypto/rand" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "math/big" + "net/smtp" + "strconv" + "sub2api/internal/model" + "sub2api/internal/repository" + "time" + + "github.com/redis/go-redis/v9" +) + +var ( + ErrEmailNotConfigured = errors.New("email service not configured") + ErrInvalidVerifyCode = errors.New("invalid or expired verification code") + ErrVerifyCodeTooFrequent = errors.New("please wait before requesting a new code") + ErrVerifyCodeMaxAttempts = errors.New("too many failed attempts, please request a new code") +) + +const ( + verifyCodeKeyPrefix = "email_verify:" + verifyCodeTTL = 15 * time.Minute + verifyCodeCooldown = 1 * time.Minute + maxVerifyCodeAttempts = 5 +) + +// verifyCodeData Redis 中存储的验证码数据 +type verifyCodeData struct { + Code string `json:"code"` + Attempts int `json:"attempts"` + CreatedAt time.Time `json:"created_at"` +} + +// SmtpConfig SMTP配置 +type SmtpConfig struct { + Host string + Port int + Username string + Password string + From string + FromName string + UseTLS bool +} + +// EmailService 邮件服务 +type EmailService struct { + settingRepo *repository.SettingRepository + rdb *redis.Client +} + +// NewEmailService 创建邮件服务实例 +func NewEmailService(settingRepo *repository.SettingRepository, rdb *redis.Client) *EmailService { + return &EmailService{ + settingRepo: settingRepo, + rdb: rdb, + } +} + +// GetSmtpConfig 从数据库获取SMTP配置 +func (s *EmailService) GetSmtpConfig(ctx context.Context) (*SmtpConfig, error) { + keys := []string{ + model.SettingKeySmtpHost, + model.SettingKeySmtpPort, + model.SettingKeySmtpUsername, + model.SettingKeySmtpPassword, + model.SettingKeySmtpFrom, + model.SettingKeySmtpFromName, + model.SettingKeySmtpUseTLS, + } + + settings, err := s.settingRepo.GetMultiple(ctx, keys) + if err != nil { + return nil, fmt.Errorf("get smtp settings: %w", err) + } + + host := settings[model.SettingKeySmtpHost] + if host == "" { + return nil, ErrEmailNotConfigured + } + + port := 587 // 默认端口 + if portStr := settings[model.SettingKeySmtpPort]; portStr != "" { + if p, err := strconv.Atoi(portStr); err == nil { + port = p + } + } + + useTLS := settings[model.SettingKeySmtpUseTLS] == "true" + + return &SmtpConfig{ + Host: host, + Port: port, + Username: settings[model.SettingKeySmtpUsername], + Password: settings[model.SettingKeySmtpPassword], + From: settings[model.SettingKeySmtpFrom], + FromName: settings[model.SettingKeySmtpFromName], + UseTLS: useTLS, + }, nil +} + +// SendEmail 发送邮件(使用数据库中保存的配置) +func (s *EmailService) SendEmail(ctx context.Context, to, subject, body string) error { + config, err := s.GetSmtpConfig(ctx) + if err != nil { + return err + } + return s.SendEmailWithConfig(config, to, subject, body) +} + +// SendEmailWithConfig 使用指定配置发送邮件 +func (s *EmailService) SendEmailWithConfig(config *SmtpConfig, to, subject, body string) error { + from := config.From + if config.FromName != "" { + from = fmt.Sprintf("%s <%s>", config.FromName, config.From) + } + + msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n%s", + from, to, subject, body) + + addr := fmt.Sprintf("%s:%d", config.Host, config.Port) + auth := smtp.PlainAuth("", config.Username, config.Password, config.Host) + + if config.UseTLS { + return s.sendMailTLS(addr, auth, config.From, to, []byte(msg), config.Host) + } + + return smtp.SendMail(addr, auth, config.From, []string{to}, []byte(msg)) +} + +// sendMailTLS 使用TLS发送邮件 +func (s *EmailService) sendMailTLS(addr string, auth smtp.Auth, from, to string, msg []byte, host string) error { + tlsConfig := &tls.Config{ + ServerName: host, + } + + conn, err := tls.Dial("tcp", addr, tlsConfig) + if err != nil { + return fmt.Errorf("tls dial: %w", err) + } + defer conn.Close() + + client, err := smtp.NewClient(conn, host) + if err != nil { + return fmt.Errorf("new smtp client: %w", err) + } + defer client.Close() + + if err = client.Auth(auth); err != nil { + return fmt.Errorf("smtp auth: %w", err) + } + + if err = client.Mail(from); err != nil { + return fmt.Errorf("smtp mail: %w", err) + } + + if err = client.Rcpt(to); err != nil { + return fmt.Errorf("smtp rcpt: %w", err) + } + + w, err := client.Data() + if err != nil { + return fmt.Errorf("smtp data: %w", err) + } + + _, err = w.Write(msg) + if err != nil { + return fmt.Errorf("write msg: %w", err) + } + + err = w.Close() + if err != nil { + return fmt.Errorf("close writer: %w", err) + } + + // Email is sent successfully after w.Close(), ignore Quit errors + // Some SMTP servers return non-standard responses on QUIT + _ = client.Quit() + return nil +} + +// GenerateVerifyCode 生成6位数字验证码 +func (s *EmailService) GenerateVerifyCode() (string, error) { + const digits = "0123456789" + code := make([]byte, 6) + for i := range code { + num, err := rand.Int(rand.Reader, big.NewInt(int64(len(digits)))) + if err != nil { + return "", err + } + code[i] = digits[num.Int64()] + } + return string(code), nil +} + +// SendVerifyCode 发送验证码邮件 +func (s *EmailService) SendVerifyCode(ctx context.Context, email, siteName string) error { + key := verifyCodeKeyPrefix + email + + // 检查是否在冷却期内 + existing, err := s.getVerifyCodeData(ctx, key) + if err == nil && existing != nil { + if time.Since(existing.CreatedAt) < verifyCodeCooldown { + return ErrVerifyCodeTooFrequent + } + } + + // 生成验证码 + code, err := s.GenerateVerifyCode() + if err != nil { + return fmt.Errorf("generate code: %w", err) + } + + // 保存验证码到 Redis + data := &verifyCodeData{ + Code: code, + Attempts: 0, + CreatedAt: time.Now(), + } + if err := s.setVerifyCodeData(ctx, key, data); err != nil { + return fmt.Errorf("save verify code: %w", err) + } + + // 构建邮件内容 + subject := fmt.Sprintf("[%s] Email Verification Code", siteName) + body := s.buildVerifyCodeEmailBody(code, siteName) + + // 发送邮件 + if err := s.SendEmail(ctx, email, subject, body); err != nil { + return fmt.Errorf("send email: %w", err) + } + + return nil +} + +// VerifyCode 验证验证码 +func (s *EmailService) VerifyCode(ctx context.Context, email, code string) error { + key := verifyCodeKeyPrefix + email + + data, err := s.getVerifyCodeData(ctx, key) + if err != nil || data == nil { + return ErrInvalidVerifyCode + } + + // 检查是否已达到最大尝试次数 + if data.Attempts >= maxVerifyCodeAttempts { + return ErrVerifyCodeMaxAttempts + } + + // 验证码不匹配 + if data.Code != code { + data.Attempts++ + _ = s.setVerifyCodeData(ctx, key, data) + if data.Attempts >= maxVerifyCodeAttempts { + return ErrVerifyCodeMaxAttempts + } + return ErrInvalidVerifyCode + } + + // 验证成功,删除验证码 + s.rdb.Del(ctx, key) + return nil +} + +// getVerifyCodeData 从 Redis 获取验证码数据 +func (s *EmailService) getVerifyCodeData(ctx context.Context, key string) (*verifyCodeData, error) { + val, err := s.rdb.Get(ctx, key).Result() + if err != nil { + return nil, err + } + var data verifyCodeData + if err := json.Unmarshal([]byte(val), &data); err != nil { + return nil, err + } + return &data, nil +} + +// setVerifyCodeData 保存验证码数据到 Redis +func (s *EmailService) setVerifyCodeData(ctx context.Context, key string, data *verifyCodeData) error { + val, err := json.Marshal(data) + if err != nil { + return err + } + return s.rdb.Set(ctx, key, val, verifyCodeTTL).Err() +} + +// buildVerifyCodeEmailBody 构建验证码邮件HTML内容 +func (s *EmailService) buildVerifyCodeEmailBody(code, siteName string) string { + return fmt.Sprintf(` + + + + + + + +
+
+

%s

+
+
+

Your verification code is:

+
%s
+
+

This code will expire in 15 minutes.

+

If you did not request this code, please ignore this email.

+
+
+ +
+ + +`, siteName, code) +} + +// TestSmtpConnectionWithConfig 使用指定配置测试SMTP连接 +func (s *EmailService) TestSmtpConnectionWithConfig(config *SmtpConfig) error { + addr := fmt.Sprintf("%s:%d", config.Host, config.Port) + + if config.UseTLS { + tlsConfig := &tls.Config{ServerName: config.Host} + conn, err := tls.Dial("tcp", addr, tlsConfig) + if err != nil { + return fmt.Errorf("tls connection failed: %w", err) + } + defer conn.Close() + + client, err := smtp.NewClient(conn, config.Host) + if err != nil { + return fmt.Errorf("smtp client creation failed: %w", err) + } + defer client.Close() + + auth := smtp.PlainAuth("", config.Username, config.Password, config.Host) + if err = client.Auth(auth); err != nil { + return fmt.Errorf("smtp authentication failed: %w", err) + } + + return client.Quit() + } + + // 非TLS连接测试 + client, err := smtp.Dial(addr) + if err != nil { + return fmt.Errorf("smtp connection failed: %w", err) + } + defer client.Close() + + auth := smtp.PlainAuth("", config.Username, config.Password, config.Host) + if err = client.Auth(auth); err != nil { + return fmt.Errorf("smtp authentication failed: %w", err) + } + + return client.Quit() +} diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go new file mode 100644 index 00000000..02fe6f21 --- /dev/null +++ b/backend/internal/service/gateway_service.go @@ -0,0 +1,1022 @@ +package service + +import ( + "bufio" + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" + "time" + + "sub2api/internal/config" + "sub2api/internal/model" + "sub2api/internal/repository" + + "github.com/gin-gonic/gin" + "github.com/redis/go-redis/v9" +) + +const ( + claudeAPIURL = "https://api.anthropic.com/v1/messages?beta=true" + stickySessionPrefix = "sticky_session:" + stickySessionTTL = time.Hour // 粘性会话TTL + tokenRefreshBuffer = 5 * 60 // 提前5分钟刷新token +) + +// allowedHeaders 白名单headers(参考CRS项目) +var allowedHeaders = map[string]bool{ + "accept": true, + "x-stainless-retry-count": true, + "x-stainless-timeout": true, + "x-stainless-lang": true, + "x-stainless-package-version": true, + "x-stainless-os": true, + "x-stainless-arch": true, + "x-stainless-runtime": true, + "x-stainless-runtime-version": true, + "x-stainless-helper-method": true, + "anthropic-dangerous-direct-browser-access": true, + "anthropic-version": true, + "x-app": true, + "anthropic-beta": true, + "accept-language": true, + "sec-fetch-mode": true, + "accept-encoding": true, + "user-agent": true, + "content-type": true, +} + +// ClaudeUsage 表示Claude API返回的usage信息 +type ClaudeUsage struct { + InputTokens int `json:"input_tokens"` + OutputTokens int `json:"output_tokens"` + CacheCreationInputTokens int `json:"cache_creation_input_tokens"` + CacheReadInputTokens int `json:"cache_read_input_tokens"` +} + +// ForwardResult 转发结果 +type ForwardResult struct { + RequestID string + Usage ClaudeUsage + Model string + Stream bool + Duration time.Duration + FirstTokenMs *int // 首字时间(流式请求) +} + +// GatewayService handles API gateway operations +type GatewayService struct { + repos *repository.Repositories + rdb *redis.Client + cfg *config.Config + oauthService *OAuthService + billingService *BillingService + rateLimitService *RateLimitService + billingCacheService *BillingCacheService + identityService *IdentityService + httpClient *http.Client +} + +// NewGatewayService creates a new GatewayService +func NewGatewayService(repos *repository.Repositories, rdb *redis.Client, cfg *config.Config, oauthService *OAuthService, billingService *BillingService, rateLimitService *RateLimitService, billingCacheService *BillingCacheService, identityService *IdentityService) *GatewayService { + // 计算响应头超时时间 + responseHeaderTimeout := time.Duration(cfg.Gateway.ResponseHeaderTimeout) * time.Second + if responseHeaderTimeout == 0 { + responseHeaderTimeout = 300 * time.Second // 默认5分钟,LLM高负载时可能排队较久 + } + + transport := &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 90 * time.Second, + ResponseHeaderTimeout: responseHeaderTimeout, // 等待上游响应头的超时 + // 注意:不设置整体 Timeout,让流式响应可以无限时间传输 + } + return &GatewayService{ + repos: repos, + rdb: rdb, + cfg: cfg, + oauthService: oauthService, + billingService: billingService, + rateLimitService: rateLimitService, + billingCacheService: billingCacheService, + identityService: identityService, + httpClient: &http.Client{ + Transport: transport, + // 不设置 Timeout:流式请求可能持续十几分钟 + // 超时控制由 Transport.ResponseHeaderTimeout 负责(只控制等待响应头) + }, + } +} + +// GenerateSessionHash 从请求体计算粘性会话hash +func (s *GatewayService) GenerateSessionHash(body []byte) string { + var req map[string]interface{} + if err := json.Unmarshal(body, &req); err != nil { + return "" + } + + // 1. 最高优先级:从metadata.user_id提取session_xxx + if metadata, ok := req["metadata"].(map[string]interface{}); ok { + if userID, ok := metadata["user_id"].(string); ok { + re := regexp.MustCompile(`session_([a-f0-9-]{36})`) + if match := re.FindStringSubmatch(userID); len(match) > 1 { + return match[1] + } + } + } + + // 2. 提取带cache_control: {type: "ephemeral"}的内容 + cacheableContent := s.extractCacheableContent(req) + if cacheableContent != "" { + return s.hashContent(cacheableContent) + } + + // 3. Fallback: 使用system内容 + if system := req["system"]; system != nil { + systemText := s.extractTextFromSystem(system) + if systemText != "" { + return s.hashContent(systemText) + } + } + + // 4. 最后fallback: 使用第一条消息 + if messages, ok := req["messages"].([]interface{}); ok && len(messages) > 0 { + if firstMsg, ok := messages[0].(map[string]interface{}); ok { + msgText := s.extractTextFromContent(firstMsg["content"]) + if msgText != "" { + return s.hashContent(msgText) + } + } + } + + return "" +} + +func (s *GatewayService) extractCacheableContent(req map[string]interface{}) string { + var content string + + // 检查system中的cacheable内容 + if system, ok := req["system"].([]interface{}); ok { + for _, part := range system { + if partMap, ok := part.(map[string]interface{}); ok { + if cc, ok := partMap["cache_control"].(map[string]interface{}); ok { + if cc["type"] == "ephemeral" { + if text, ok := partMap["text"].(string); ok { + content += text + } + } + } + } + } + } + + // 检查messages中的cacheable内容 + if messages, ok := req["messages"].([]interface{}); ok { + for _, msg := range messages { + if msgMap, ok := msg.(map[string]interface{}); ok { + if msgContent, ok := msgMap["content"].([]interface{}); ok { + for _, part := range msgContent { + if partMap, ok := part.(map[string]interface{}); ok { + if cc, ok := partMap["cache_control"].(map[string]interface{}); ok { + if cc["type"] == "ephemeral" { + // 找到cacheable内容,提取第一条消息的文本 + return s.extractTextFromContent(msgMap["content"]) + } + } + } + } + } + } + } + } + + return content +} + +func (s *GatewayService) extractTextFromSystem(system interface{}) string { + switch v := system.(type) { + case string: + return v + case []interface{}: + var texts []string + for _, part := range v { + if partMap, ok := part.(map[string]interface{}); ok { + if text, ok := partMap["text"].(string); ok { + texts = append(texts, text) + } + } + } + return strings.Join(texts, "") + } + return "" +} + +func (s *GatewayService) extractTextFromContent(content interface{}) string { + switch v := content.(type) { + case string: + return v + case []interface{}: + var texts []string + for _, part := range v { + if partMap, ok := part.(map[string]interface{}); ok { + if partMap["type"] == "text" { + if text, ok := partMap["text"].(string); ok { + texts = append(texts, text) + } + } + } + } + return strings.Join(texts, "") + } + return "" +} + +func (s *GatewayService) hashContent(content string) string { + hash := sha256.Sum256([]byte(content)) + return hex.EncodeToString(hash[:16]) // 32字符 +} + +// replaceModelInBody 替换请求体中的model字段 +func (s *GatewayService) replaceModelInBody(body []byte, newModel string) []byte { + var req map[string]interface{} + if err := json.Unmarshal(body, &req); err != nil { + return body + } + req["model"] = newModel + newBody, err := json.Marshal(req) + if err != nil { + return body + } + return newBody +} + +// SelectAccount 选择账号(粘性会话+优先级) +func (s *GatewayService) SelectAccount(ctx context.Context, groupID *int64, sessionHash string) (*model.Account, error) { + return s.SelectAccountForModel(ctx, groupID, sessionHash, "") +} + +// SelectAccountForModel 选择支持指定模型的账号(粘性会话+优先级+模型映射) +func (s *GatewayService) SelectAccountForModel(ctx context.Context, groupID *int64, sessionHash string, requestedModel string) (*model.Account, error) { + // 1. 查询粘性会话 + if sessionHash != "" { + accountID, err := s.rdb.Get(ctx, stickySessionPrefix+sessionHash).Int64() + if err == nil && accountID > 0 { + account, err := s.repos.Account.GetByID(ctx, accountID) + // 使用IsSchedulable代替IsActive,确保限流/过载账号不会被选中 + // 同时检查模型支持 + if err == nil && account.IsSchedulable() && (requestedModel == "" || account.IsModelSupported(requestedModel)) { + // 续期粘性会话 + s.rdb.Expire(ctx, stickySessionPrefix+sessionHash, stickySessionTTL) + return account, nil + } + } + } + + // 2. 获取可调度账号列表(排除限流和过载的账号) + var accounts []model.Account + var err error + if groupID != nil { + accounts, err = s.repos.Account.ListSchedulableByGroupID(ctx, *groupID) + } else { + accounts, err = s.repos.Account.ListSchedulable(ctx) + } + if err != nil { + return nil, fmt.Errorf("query accounts failed: %w", err) + } + + // 3. 按优先级+最久未用选择(考虑模型支持) + var selected *model.Account + for i := range accounts { + acc := &accounts[i] + // 检查模型支持 + if requestedModel != "" && !acc.IsModelSupported(requestedModel) { + continue + } + if selected == nil { + selected = acc + continue + } + // 优先选择priority值更小的(priority值越小优先级越高) + if acc.Priority < selected.Priority { + selected = acc + } else if acc.Priority == selected.Priority { + // 优先级相同时,选最久未用的 + if acc.LastUsedAt == nil || (selected.LastUsedAt != nil && acc.LastUsedAt.Before(*selected.LastUsedAt)) { + selected = acc + } + } + } + + if selected == nil { + if requestedModel != "" { + return nil, fmt.Errorf("no available accounts supporting model: %s", requestedModel) + } + return nil, errors.New("no available accounts") + } + + // 4. 建立粘性绑定 + if sessionHash != "" { + s.rdb.Set(ctx, stickySessionPrefix+sessionHash, selected.ID, stickySessionTTL) + } + + return selected, nil +} + +// GetAccessToken 获取账号凭证 +func (s *GatewayService) GetAccessToken(ctx context.Context, account *model.Account) (string, string, error) { + switch account.Type { + case model.AccountTypeOAuth, model.AccountTypeSetupToken: + // Both oauth and setup-token use OAuth token flow + return s.getOAuthToken(ctx, account) + case model.AccountTypeApiKey: + apiKey := account.GetCredential("api_key") + if apiKey == "" { + return "", "", errors.New("api_key not found in credentials") + } + return apiKey, "apikey", nil + default: + return "", "", fmt.Errorf("unsupported account type: %s", account.Type) + } +} + +func (s *GatewayService) getOAuthToken(ctx context.Context, account *model.Account) (string, string, error) { + accessToken := account.GetCredential("access_token") + expiresAtStr := account.GetCredential("expires_at") + + // 检查是否需要刷新 + needRefresh := false + if expiresAtStr != "" { + expiresAt, err := strconv.ParseInt(expiresAtStr, 10, 64) + if err == nil && time.Now().Unix()+tokenRefreshBuffer > expiresAt { + needRefresh = true + } + } + + if needRefresh || accessToken == "" { + tokenInfo, err := s.oauthService.RefreshAccountToken(ctx, account) + if err != nil { + return "", "", fmt.Errorf("refresh token failed: %w", err) + } + + // 更新账号凭证 + account.Credentials["access_token"] = tokenInfo.AccessToken + account.Credentials["expires_at"] = strconv.FormatInt(tokenInfo.ExpiresAt, 10) + if tokenInfo.RefreshToken != "" { + account.Credentials["refresh_token"] = tokenInfo.RefreshToken + } + + if err := s.repos.Account.Update(ctx, account); err != nil { + log.Printf("Failed to update account credentials: %v", err) + } + + return tokenInfo.AccessToken, "oauth", nil + } + + return accessToken, "oauth", nil +} + +// Forward 转发请求到Claude API +func (s *GatewayService) Forward(ctx context.Context, c *gin.Context, account *model.Account, body []byte) (*ForwardResult, error) { + startTime := time.Now() + + // 解析请求获取model和stream + var req struct { + Model string `json:"model"` + Stream bool `json:"stream"` + } + if err := json.Unmarshal(body, &req); err != nil { + return nil, fmt.Errorf("parse request: %w", err) + } + + // 应用模型映射(仅对apikey类型账号) + originalModel := req.Model + if account.Type == model.AccountTypeApiKey { + mappedModel := account.GetMappedModel(req.Model) + if mappedModel != req.Model { + // 替换请求体中的模型名 + body = s.replaceModelInBody(body, mappedModel) + req.Model = mappedModel + log.Printf("Model mapping applied: %s -> %s (account: %s)", originalModel, mappedModel, account.Name) + } + } + + // 获取凭证 + token, tokenType, err := s.GetAccessToken(ctx, account) + if err != nil { + return nil, err + } + + // 构建上游请求 + upstreamReq, err := s.buildUpstreamRequest(ctx, c, account, body, token, tokenType) + if err != nil { + return nil, err + } + + // 发送请求 + resp, err := s.httpClient.Do(upstreamReq) + if err != nil { + return nil, fmt.Errorf("upstream request failed: %w", err) + } + defer resp.Body.Close() + + // 处理401错误:刷新token重试 + if resp.StatusCode == http.StatusUnauthorized && tokenType == "oauth" { + resp.Body.Close() + token, tokenType, err = s.forceRefreshToken(ctx, account) + if err != nil { + return nil, fmt.Errorf("token refresh failed: %w", err) + } + upstreamReq, err = s.buildUpstreamRequest(ctx, c, account, body, token, tokenType) + if err != nil { + return nil, err + } + resp, err = s.httpClient.Do(upstreamReq) + if err != nil { + return nil, fmt.Errorf("retry request failed: %w", err) + } + defer resp.Body.Close() + } + + // 处理错误响应 + if resp.StatusCode >= 400 { + return s.handleErrorResponse(ctx, resp, c, account) + } + + // 处理正常响应 + var usage *ClaudeUsage + var firstTokenMs *int + if req.Stream { + streamResult, err := s.handleStreamingResponse(ctx, resp, c, account, startTime, originalModel, req.Model) + if err != nil { + return nil, err + } + usage = streamResult.usage + firstTokenMs = streamResult.firstTokenMs + } else { + usage, err = s.handleNonStreamingResponse(ctx, resp, c, account, originalModel, req.Model) + if err != nil { + return nil, err + } + } + + return &ForwardResult{ + RequestID: resp.Header.Get("x-request-id"), + Usage: *usage, + Model: originalModel, // 使用原始模型用于计费和日志 + Stream: req.Stream, + Duration: time.Since(startTime), + FirstTokenMs: firstTokenMs, + }, nil +} + +func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Context, account *model.Account, body []byte, token, tokenType string) (*http.Request, error) { + // 确定目标URL + targetURL := claudeAPIURL + if account.Type == model.AccountTypeApiKey { + baseURL := account.GetBaseURL() + targetURL = baseURL + "/v1/messages" + } + + // OAuth账号:应用统一指纹 + var fingerprint *Fingerprint + if account.IsOAuth() && s.identityService != nil { + // 1. 获取或创建指纹(包含随机生成的ClientID) + fp, err := s.identityService.GetOrCreateFingerprint(ctx, account.ID, c.Request.Header) + if err != nil { + log.Printf("Warning: failed to get fingerprint for account %d: %v", account.ID, err) + // 失败时降级为透传原始headers + } else { + fingerprint = fp + + // 2. 重写metadata.user_id(需要指纹中的ClientID和账号的account_uuid) + accountUUID := account.GetExtraString("account_uuid") + if accountUUID != "" && fp.ClientID != "" { + if newBody, err := s.identityService.RewriteUserID(body, account.ID, accountUUID, fp.ClientID); err == nil && len(newBody) > 0 { + body = newBody + } + } + } + } + + req, err := http.NewRequestWithContext(ctx, "POST", targetURL, bytes.NewReader(body)) + if err != nil { + return nil, err + } + + // 设置认证头 + if tokenType == "oauth" { + req.Header.Set("Authorization", "Bearer "+token) + } else { + req.Header.Set("x-api-key", token) + } + + // 白名单透传headers + for key, values := range c.Request.Header { + lowerKey := strings.ToLower(key) + if allowedHeaders[lowerKey] { + for _, v := range values { + req.Header.Add(key, v) + } + } + } + + // OAuth账号:应用缓存的指纹到请求头(覆盖白名单透传的头) + if fingerprint != nil { + s.identityService.ApplyFingerprint(req, fingerprint) + } + + // 确保必要的headers存在 + if req.Header.Get("Content-Type") == "" { + req.Header.Set("Content-Type", "application/json") + } + if req.Header.Get("anthropic-version") == "" { + req.Header.Set("anthropic-version", "2023-06-01") + } + + // 处理anthropic-beta header(OAuth账号需要特殊处理) + if tokenType == "oauth" { + req.Header.Set("anthropic-beta", s.getBetaHeader(body, c.GetHeader("anthropic-beta"))) + } + + // 配置代理 + if account.ProxyID != nil && account.Proxy != nil { + proxyURL := account.Proxy.URL() + if proxyURL != "" { + if parsedURL, err := url.Parse(proxyURL); err == nil { + // 计算响应头超时时间(与默认 Transport 保持一致) + responseHeaderTimeout := time.Duration(s.cfg.Gateway.ResponseHeaderTimeout) * time.Second + if responseHeaderTimeout == 0 { + responseHeaderTimeout = 300 * time.Second + } + transport := &http.Transport{ + Proxy: http.ProxyURL(parsedURL), + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 90 * time.Second, + ResponseHeaderTimeout: responseHeaderTimeout, + } + s.httpClient.Transport = transport + } + } + } + + return req, nil +} + +// getBetaHeader 处理anthropic-beta header +// 对于OAuth账号,需要确保包含oauth-2025-04-20 +func (s *GatewayService) getBetaHeader(body []byte, clientBetaHeader string) string { + const oauthBeta = "oauth-2025-04-20" + const claudeCodeBeta = "claude-code-20250219" + + // 如果客户端传了anthropic-beta + if clientBetaHeader != "" { + // 已包含oauth beta则直接返回 + if strings.Contains(clientBetaHeader, oauthBeta) { + return clientBetaHeader + } + + // 需要添加oauth beta + parts := strings.Split(clientBetaHeader, ",") + for i, p := range parts { + parts[i] = strings.TrimSpace(p) + } + + // 在claude-code-20250219后面插入oauth beta + claudeCodeIdx := -1 + for i, p := range parts { + if p == claudeCodeBeta { + claudeCodeIdx = i + break + } + } + + if claudeCodeIdx >= 0 { + // 在claude-code后面插入 + newParts := make([]string, 0, len(parts)+1) + newParts = append(newParts, parts[:claudeCodeIdx+1]...) + newParts = append(newParts, oauthBeta) + newParts = append(newParts, parts[claudeCodeIdx+1:]...) + return strings.Join(newParts, ",") + } + + // 没有claude-code,放在第一位 + return oauthBeta + "," + clientBetaHeader + } + + // 客户端没传,根据模型生成 + var modelID string + var reqMap map[string]interface{} + if json.Unmarshal(body, &reqMap) == nil { + if m, ok := reqMap["model"].(string); ok { + modelID = m + } + } + + // haiku模型不需要claude-code beta + if strings.Contains(strings.ToLower(modelID), "haiku") { + return "oauth-2025-04-20,interleaved-thinking-2025-05-14" + } + + return "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14" +} + +func (s *GatewayService) forceRefreshToken(ctx context.Context, account *model.Account) (string, string, error) { + tokenInfo, err := s.oauthService.RefreshAccountToken(ctx, account) + if err != nil { + return "", "", err + } + + account.Credentials["access_token"] = tokenInfo.AccessToken + account.Credentials["expires_at"] = strconv.FormatInt(tokenInfo.ExpiresAt, 10) + if tokenInfo.RefreshToken != "" { + account.Credentials["refresh_token"] = tokenInfo.RefreshToken + } + + if err := s.repos.Account.Update(ctx, account); err != nil { + log.Printf("Failed to update account credentials: %v", err) + } + + return tokenInfo.AccessToken, "oauth", nil +} + +func (s *GatewayService) handleErrorResponse(ctx context.Context, resp *http.Response, c *gin.Context, account *model.Account) (*ForwardResult, error) { + body, _ := io.ReadAll(resp.Body) + + // apikey 类型账号:检查自定义错误码配置 + // 如果启用且错误码不在列表中,返回通用 500 错误(不做任何账号状态处理) + if !account.ShouldHandleErrorCode(resp.StatusCode) { + c.JSON(http.StatusInternalServerError, gin.H{ + "type": "error", + "error": gin.H{ + "type": "upstream_error", + "message": "Upstream gateway error", + }, + }) + return nil, fmt.Errorf("upstream error: %d (not in custom error codes)", resp.StatusCode) + } + + // 处理上游错误,标记账号状态 + s.rateLimitService.HandleUpstreamError(ctx, account, resp.StatusCode, resp.Header, body) + + // 根据状态码返回适当的自定义错误响应(不透传上游详细信息) + var errType, errMsg string + var statusCode int + + switch resp.StatusCode { + case 401: + statusCode = http.StatusBadGateway + errType = "upstream_error" + errMsg = "Upstream authentication failed, please contact administrator" + case 403: + statusCode = http.StatusBadGateway + errType = "upstream_error" + errMsg = "Upstream access forbidden, please contact administrator" + case 429: + statusCode = http.StatusTooManyRequests + errType = "rate_limit_error" + errMsg = "Upstream rate limit exceeded, please retry later" + case 529: + statusCode = http.StatusServiceUnavailable + errType = "overloaded_error" + errMsg = "Upstream service overloaded, please retry later" + case 500, 502, 503, 504: + statusCode = http.StatusBadGateway + errType = "upstream_error" + errMsg = "Upstream service temporarily unavailable" + default: + statusCode = http.StatusBadGateway + errType = "upstream_error" + errMsg = "Upstream request failed" + } + + // 返回自定义错误响应 + c.JSON(statusCode, gin.H{ + "type": "error", + "error": gin.H{ + "type": errType, + "message": errMsg, + }, + }) + + return nil, fmt.Errorf("upstream error: %d", resp.StatusCode) +} + +// streamingResult 流式响应结果 +type streamingResult struct { + usage *ClaudeUsage + firstTokenMs *int +} + +func (s *GatewayService) handleStreamingResponse(ctx context.Context, resp *http.Response, c *gin.Context, account *model.Account, startTime time.Time, originalModel, mappedModel string) (*streamingResult, error) { + // 更新5h窗口状态 + s.rateLimitService.UpdateSessionWindow(ctx, account, resp.Header) + + // 设置SSE响应头 + c.Header("Content-Type", "text/event-stream") + c.Header("Cache-Control", "no-cache") + c.Header("Connection", "keep-alive") + c.Header("X-Accel-Buffering", "no") + + // 透传其他响应头 + if v := resp.Header.Get("x-request-id"); v != "" { + c.Header("x-request-id", v) + } + + w := c.Writer + flusher, ok := w.(http.Flusher) + if !ok { + return nil, errors.New("streaming not supported") + } + + usage := &ClaudeUsage{} + var firstTokenMs *int + scanner := bufio.NewScanner(resp.Body) + // 设置更大的buffer以处理长行 + scanner.Buffer(make([]byte, 64*1024), 1024*1024) + + needModelReplace := originalModel != mappedModel + + for scanner.Scan() { + line := scanner.Text() + + // 如果有模型映射,替换响应中的model字段 + if needModelReplace && strings.HasPrefix(line, "data: ") { + line = s.replaceModelInSSELine(line, mappedModel, originalModel) + } + + // 转发行 + fmt.Fprintf(w, "%s\n", line) + flusher.Flush() + + // 解析usage数据 + if strings.HasPrefix(line, "data: ") { + data := line[6:] + // 记录首字时间:第一个有效的 content_block_delta 或 message_start + if firstTokenMs == nil && data != "" && data != "[DONE]" { + ms := int(time.Since(startTime).Milliseconds()) + firstTokenMs = &ms + } + s.parseSSEUsage(data, usage) + } + } + + if err := scanner.Err(); err != nil { + return &streamingResult{usage: usage, firstTokenMs: firstTokenMs}, fmt.Errorf("stream read error: %w", err) + } + + return &streamingResult{usage: usage, firstTokenMs: firstTokenMs}, nil +} + +// replaceModelInSSELine 替换SSE数据行中的model字段 +func (s *GatewayService) replaceModelInSSELine(line, fromModel, toModel string) string { + data := line[6:] // 去掉 "data: " 前缀 + if data == "" || data == "[DONE]" { + return line + } + + var event map[string]interface{} + if err := json.Unmarshal([]byte(data), &event); err != nil { + return line + } + + // 只替换 message_start 事件中的 message.model + if event["type"] != "message_start" { + return line + } + + msg, ok := event["message"].(map[string]interface{}) + if !ok { + return line + } + + model, ok := msg["model"].(string) + if !ok || model != fromModel { + return line + } + + msg["model"] = toModel + newData, err := json.Marshal(event) + if err != nil { + return line + } + + return "data: " + string(newData) +} + +func (s *GatewayService) parseSSEUsage(data string, usage *ClaudeUsage) { + // 解析message_start获取input tokens + var msgStart struct { + Type string `json:"type"` + Message struct { + Usage ClaudeUsage `json:"usage"` + } `json:"message"` + } + if json.Unmarshal([]byte(data), &msgStart) == nil && msgStart.Type == "message_start" { + usage.InputTokens = msgStart.Message.Usage.InputTokens + usage.CacheCreationInputTokens = msgStart.Message.Usage.CacheCreationInputTokens + usage.CacheReadInputTokens = msgStart.Message.Usage.CacheReadInputTokens + } + + // 解析message_delta获取output tokens + var msgDelta struct { + Type string `json:"type"` + Usage struct { + OutputTokens int `json:"output_tokens"` + } `json:"usage"` + } + if json.Unmarshal([]byte(data), &msgDelta) == nil && msgDelta.Type == "message_delta" { + usage.OutputTokens = msgDelta.Usage.OutputTokens + } +} + +func (s *GatewayService) handleNonStreamingResponse(ctx context.Context, resp *http.Response, c *gin.Context, account *model.Account, originalModel, mappedModel string) (*ClaudeUsage, error) { + // 更新5h窗口状态 + s.rateLimitService.UpdateSessionWindow(ctx, account, resp.Header) + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + // 解析usage + var response struct { + Usage ClaudeUsage `json:"usage"` + } + if err := json.Unmarshal(body, &response); err != nil { + return nil, fmt.Errorf("parse response: %w", err) + } + + // 如果有模型映射,替换响应中的model字段 + if originalModel != mappedModel { + body = s.replaceModelInResponseBody(body, mappedModel, originalModel) + } + + // 透传响应头 + for key, values := range resp.Header { + for _, value := range values { + c.Header(key, value) + } + } + + // 写入响应 + c.Data(resp.StatusCode, "application/json", body) + + return &response.Usage, nil +} + +// replaceModelInResponseBody 替换响应体中的model字段 +func (s *GatewayService) replaceModelInResponseBody(body []byte, fromModel, toModel string) []byte { + var resp map[string]interface{} + if err := json.Unmarshal(body, &resp); err != nil { + return body + } + + model, ok := resp["model"].(string) + if !ok || model != fromModel { + return body + } + + resp["model"] = toModel + newBody, err := json.Marshal(resp) + if err != nil { + return body + } + + return newBody +} + +// RecordUsageInput 记录使用量的输入参数 +type RecordUsageInput struct { + Result *ForwardResult + ApiKey *model.ApiKey + User *model.User + Account *model.Account + Subscription *model.UserSubscription // 可选:订阅信息 +} + +// RecordUsage 记录使用量并扣费(或更新订阅用量) +func (s *GatewayService) RecordUsage(ctx context.Context, input *RecordUsageInput) error { + result := input.Result + apiKey := input.ApiKey + user := input.User + account := input.Account + subscription := input.Subscription + + // 计算费用 + tokens := UsageTokens{ + InputTokens: result.Usage.InputTokens, + OutputTokens: result.Usage.OutputTokens, + CacheCreationTokens: result.Usage.CacheCreationInputTokens, + CacheReadTokens: result.Usage.CacheReadInputTokens, + } + + // 获取费率倍数 + multiplier := s.cfg.Default.RateMultiplier + if apiKey.GroupID != nil && apiKey.Group != nil { + multiplier = apiKey.Group.RateMultiplier + } + + cost, err := s.billingService.CalculateCost(result.Model, tokens, multiplier) + if err != nil { + log.Printf("Calculate cost failed: %v", err) + // 使用默认费用继续 + cost = &CostBreakdown{ActualCost: 0} + } + + // 判断计费方式:订阅模式 vs 余额模式 + isSubscriptionBilling := subscription != nil && apiKey.Group != nil && apiKey.Group.IsSubscriptionType() + billingType := model.BillingTypeBalance + if isSubscriptionBilling { + billingType = model.BillingTypeSubscription + } + + // 创建使用日志 + durationMs := int(result.Duration.Milliseconds()) + usageLog := &model.UsageLog{ + UserID: user.ID, + ApiKeyID: apiKey.ID, + AccountID: account.ID, + RequestID: result.RequestID, + Model: result.Model, + InputTokens: result.Usage.InputTokens, + OutputTokens: result.Usage.OutputTokens, + CacheCreationTokens: result.Usage.CacheCreationInputTokens, + CacheReadTokens: result.Usage.CacheReadInputTokens, + InputCost: cost.InputCost, + OutputCost: cost.OutputCost, + CacheCreationCost: cost.CacheCreationCost, + CacheReadCost: cost.CacheReadCost, + TotalCost: cost.TotalCost, + ActualCost: cost.ActualCost, + RateMultiplier: multiplier, + BillingType: billingType, + Stream: result.Stream, + DurationMs: &durationMs, + FirstTokenMs: result.FirstTokenMs, + CreatedAt: time.Now(), + } + + // 添加分组和订阅关联 + if apiKey.GroupID != nil { + usageLog.GroupID = apiKey.GroupID + } + if subscription != nil { + usageLog.SubscriptionID = &subscription.ID + } + + if err := s.repos.UsageLog.Create(ctx, usageLog); err != nil { + log.Printf("Create usage log failed: %v", err) + } + + // 根据计费类型执行扣费 + if isSubscriptionBilling { + // 订阅模式:更新订阅用量(使用 TotalCost 原始费用,不考虑倍率) + if cost.TotalCost > 0 { + if err := s.repos.UserSubscription.IncrementUsage(ctx, subscription.ID, cost.TotalCost); err != nil { + log.Printf("Increment subscription usage failed: %v", err) + } + // 异步更新订阅缓存 + go func() { + cacheCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := s.billingCacheService.UpdateSubscriptionUsage(cacheCtx, user.ID, *apiKey.GroupID, cost.TotalCost); err != nil { + log.Printf("Update subscription cache failed: %v", err) + } + }() + } + } else { + // 余额模式:扣除用户余额(使用 ActualCost 考虑倍率后的费用) + if cost.ActualCost > 0 { + if err := s.repos.User.DeductBalance(ctx, user.ID, cost.ActualCost); err != nil { + log.Printf("Deduct balance failed: %v", err) + } + // 异步更新余额缓存 + go func() { + cacheCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := s.billingCacheService.DeductBalanceCache(cacheCtx, user.ID, cost.ActualCost); err != nil { + log.Printf("Update balance cache failed: %v", err) + } + }() + } + } + + // 更新账号最后使用时间 + if err := s.repos.Account.UpdateLastUsed(ctx, account.ID); err != nil { + log.Printf("Update last used failed: %v", err) + } + + return nil +} diff --git a/backend/internal/service/group_service.go b/backend/internal/service/group_service.go new file mode 100644 index 00000000..9c74c626 --- /dev/null +++ b/backend/internal/service/group_service.go @@ -0,0 +1,194 @@ +package service + +import ( + "context" + "errors" + "fmt" + "sub2api/internal/model" + "sub2api/internal/repository" + + "gorm.io/gorm" +) + +var ( + ErrGroupNotFound = errors.New("group not found") + ErrGroupExists = errors.New("group name already exists") +) + +// CreateGroupRequest 创建分组请求 +type CreateGroupRequest struct { + Name string `json:"name"` + Description string `json:"description"` + RateMultiplier float64 `json:"rate_multiplier"` + IsExclusive bool `json:"is_exclusive"` +} + +// UpdateGroupRequest 更新分组请求 +type UpdateGroupRequest struct { + Name *string `json:"name"` + Description *string `json:"description"` + RateMultiplier *float64 `json:"rate_multiplier"` + IsExclusive *bool `json:"is_exclusive"` + Status *string `json:"status"` +} + +// GroupService 分组管理服务 +type GroupService struct { + groupRepo *repository.GroupRepository +} + +// NewGroupService 创建分组服务实例 +func NewGroupService(groupRepo *repository.GroupRepository) *GroupService { + return &GroupService{ + groupRepo: groupRepo, + } +} + +// Create 创建分组 +func (s *GroupService) Create(ctx context.Context, req CreateGroupRequest) (*model.Group, error) { + // 检查名称是否已存在 + exists, err := s.groupRepo.ExistsByName(ctx, req.Name) + if err != nil { + return nil, fmt.Errorf("check group exists: %w", err) + } + if exists { + return nil, ErrGroupExists + } + + // 创建分组 + group := &model.Group{ + Name: req.Name, + Description: req.Description, + RateMultiplier: req.RateMultiplier, + IsExclusive: req.IsExclusive, + Status: model.StatusActive, + } + + if err := s.groupRepo.Create(ctx, group); err != nil { + return nil, fmt.Errorf("create group: %w", err) + } + + return group, nil +} + +// GetByID 根据ID获取分组 +func (s *GroupService) GetByID(ctx context.Context, id int64) (*model.Group, error) { + group, err := s.groupRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrGroupNotFound + } + return nil, fmt.Errorf("get group: %w", err) + } + return group, nil +} + +// List 获取分组列表 +func (s *GroupService) List(ctx context.Context, params repository.PaginationParams) ([]model.Group, *repository.PaginationResult, error) { + groups, pagination, err := s.groupRepo.List(ctx, params) + if err != nil { + return nil, nil, fmt.Errorf("list groups: %w", err) + } + return groups, pagination, nil +} + +// ListActive 获取活跃分组列表 +func (s *GroupService) ListActive(ctx context.Context) ([]model.Group, error) { + groups, err := s.groupRepo.ListActive(ctx) + if err != nil { + return nil, fmt.Errorf("list active groups: %w", err) + } + return groups, nil +} + +// Update 更新分组 +func (s *GroupService) Update(ctx context.Context, id int64, req UpdateGroupRequest) (*model.Group, error) { + group, err := s.groupRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrGroupNotFound + } + return nil, fmt.Errorf("get group: %w", err) + } + + // 更新字段 + if req.Name != nil && *req.Name != group.Name { + // 检查新名称是否已存在 + exists, err := s.groupRepo.ExistsByName(ctx, *req.Name) + if err != nil { + return nil, fmt.Errorf("check group exists: %w", err) + } + if exists { + return nil, ErrGroupExists + } + group.Name = *req.Name + } + + if req.Description != nil { + group.Description = *req.Description + } + + if req.RateMultiplier != nil { + group.RateMultiplier = *req.RateMultiplier + } + + if req.IsExclusive != nil { + group.IsExclusive = *req.IsExclusive + } + + if req.Status != nil { + group.Status = *req.Status + } + + if err := s.groupRepo.Update(ctx, group); err != nil { + return nil, fmt.Errorf("update group: %w", err) + } + + return group, nil +} + +// Delete 删除分组 +func (s *GroupService) Delete(ctx context.Context, id int64) error { + // 检查分组是否存在 + _, err := s.groupRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrGroupNotFound + } + return fmt.Errorf("get group: %w", err) + } + + if err := s.groupRepo.Delete(ctx, id); err != nil { + return fmt.Errorf("delete group: %w", err) + } + + return nil +} + +// GetStats 获取分组统计信息 +func (s *GroupService) GetStats(ctx context.Context, id int64) (map[string]interface{}, error) { + group, err := s.groupRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrGroupNotFound + } + return nil, fmt.Errorf("get group: %w", err) + } + + // 获取账号数量 + accountCount, err := s.groupRepo.GetAccountCount(ctx, id) + if err != nil { + return nil, fmt.Errorf("get account count: %w", err) + } + + stats := map[string]interface{}{ + "id": group.ID, + "name": group.Name, + "rate_multiplier": group.RateMultiplier, + "is_exclusive": group.IsExclusive, + "status": group.Status, + "account_count": accountCount, + } + + return stats, nil +} diff --git a/backend/internal/service/identity_service.go b/backend/internal/service/identity_service.go new file mode 100644 index 00000000..022579d3 --- /dev/null +++ b/backend/internal/service/identity_service.go @@ -0,0 +1,282 @@ +package service + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "log" + "net/http" + "regexp" + "strconv" + "time" + + "github.com/redis/go-redis/v9" +) + +const ( + // Redis key prefix + identityFingerprintKey = "identity:fingerprint:" +) + +// 预编译正则表达式(避免每次调用重新编译) +var ( + // 匹配 user_id 格式: user_{64位hex}_account__session_{uuid} + userIDRegex = regexp.MustCompile(`^user_[a-f0-9]{64}_account__session_([a-f0-9-]{36})$`) + // 匹配 User-Agent 版本号: xxx/x.y.z + userAgentVersionRegex = regexp.MustCompile(`/(\d+)\.(\d+)\.(\d+)`) +) + +// Fingerprint 存储的指纹数据结构 +type Fingerprint struct { + ClientID string `json:"client_id"` // 64位hex客户端ID(首次随机生成) + UserAgent string `json:"user_agent"` // User-Agent + StainlessLang string `json:"x_stainless_lang"` // x-stainless-lang + StainlessPackageVersion string `json:"x_stainless_package_version"` // x-stainless-package-version + StainlessOS string `json:"x_stainless_os"` // x-stainless-os + StainlessArch string `json:"x_stainless_arch"` // x-stainless-arch + StainlessRuntime string `json:"x_stainless_runtime"` // x-stainless-runtime + StainlessRuntimeVersion string `json:"x_stainless_runtime_version"` // x-stainless-runtime-version +} + +// 默认指纹值(当客户端未提供时使用) +var defaultFingerprint = Fingerprint{ + UserAgent: "claude-cli/2.0.62 (external, cli)", + StainlessLang: "js", + StainlessPackageVersion: "0.52.0", + StainlessOS: "Linux", + StainlessArch: "x64", + StainlessRuntime: "node", + StainlessRuntimeVersion: "v22.14.0", +} + +// IdentityService 管理OAuth账号的请求身份指纹 +type IdentityService struct { + rdb *redis.Client +} + +// NewIdentityService 创建新的IdentityService +func NewIdentityService(rdb *redis.Client) *IdentityService { + return &IdentityService{rdb: rdb} +} + +// GetOrCreateFingerprint 获取或创建账号的指纹 +// 如果缓存存在,检测user-agent版本,新版本则更新 +// 如果缓存不存在,生成随机ClientID并从请求头创建指纹,然后缓存 +func (s *IdentityService) GetOrCreateFingerprint(ctx context.Context, accountID int64, headers http.Header) (*Fingerprint, error) { + key := identityFingerprintKey + strconv.FormatInt(accountID, 10) + + // 尝试从Redis获取缓存的指纹 + data, err := s.rdb.Get(ctx, key).Bytes() + if err == nil && len(data) > 0 { + // 缓存存在,解析指纹 + var cached Fingerprint + if err := json.Unmarshal(data, &cached); err == nil { + // 检查客户端的user-agent是否是更新版本 + clientUA := headers.Get("User-Agent") + if clientUA != "" && isNewerVersion(clientUA, cached.UserAgent) { + // 更新user-agent + cached.UserAgent = clientUA + // 保存更新后的指纹 + if newData, err := json.Marshal(cached); err == nil { + s.rdb.Set(ctx, key, newData, 0) // 永不过期 + } + log.Printf("Updated fingerprint user-agent for account %d: %s", accountID, clientUA) + } + return &cached, nil + } + } + + // 缓存不存在或解析失败,创建新指纹 + fp := s.createFingerprintFromHeaders(headers) + + // 生成随机ClientID + fp.ClientID = generateClientID() + + // 保存到Redis(永不过期) + if data, err := json.Marshal(fp); err == nil { + if err := s.rdb.Set(ctx, key, data, 0).Err(); err != nil { + log.Printf("Warning: failed to cache fingerprint for account %d: %v", accountID, err) + } + } + + log.Printf("Created new fingerprint for account %d with client_id: %s", accountID, fp.ClientID) + return fp, nil +} + +// createFingerprintFromHeaders 从请求头创建指纹 +func (s *IdentityService) createFingerprintFromHeaders(headers http.Header) *Fingerprint { + fp := &Fingerprint{} + + // 获取User-Agent + if ua := headers.Get("User-Agent"); ua != "" { + fp.UserAgent = ua + } else { + fp.UserAgent = defaultFingerprint.UserAgent + } + + // 获取x-stainless-*头,如果没有则使用默认值 + fp.StainlessLang = getHeaderOrDefault(headers, "X-Stainless-Lang", defaultFingerprint.StainlessLang) + fp.StainlessPackageVersion = getHeaderOrDefault(headers, "X-Stainless-Package-Version", defaultFingerprint.StainlessPackageVersion) + fp.StainlessOS = getHeaderOrDefault(headers, "X-Stainless-OS", defaultFingerprint.StainlessOS) + fp.StainlessArch = getHeaderOrDefault(headers, "X-Stainless-Arch", defaultFingerprint.StainlessArch) + fp.StainlessRuntime = getHeaderOrDefault(headers, "X-Stainless-Runtime", defaultFingerprint.StainlessRuntime) + fp.StainlessRuntimeVersion = getHeaderOrDefault(headers, "X-Stainless-Runtime-Version", defaultFingerprint.StainlessRuntimeVersion) + + return fp +} + +// getHeaderOrDefault 获取header值,如果不存在则返回默认值 +func getHeaderOrDefault(headers http.Header, key, defaultValue string) string { + if v := headers.Get(key); v != "" { + return v + } + return defaultValue +} + +// ApplyFingerprint 将指纹应用到请求头(覆盖原有的x-stainless-*头) +func (s *IdentityService) ApplyFingerprint(req *http.Request, fp *Fingerprint) { + if fp == nil { + return + } + + // 设置User-Agent + if fp.UserAgent != "" { + req.Header.Set("User-Agent", fp.UserAgent) + } + + // 设置x-stainless-*头(使用正确的大小写) + if fp.StainlessLang != "" { + req.Header.Set("X-Stainless-Lang", fp.StainlessLang) + } + if fp.StainlessPackageVersion != "" { + req.Header.Set("X-Stainless-Package-Version", fp.StainlessPackageVersion) + } + if fp.StainlessOS != "" { + req.Header.Set("X-Stainless-OS", fp.StainlessOS) + } + if fp.StainlessArch != "" { + req.Header.Set("X-Stainless-Arch", fp.StainlessArch) + } + if fp.StainlessRuntime != "" { + req.Header.Set("X-Stainless-Runtime", fp.StainlessRuntime) + } + if fp.StainlessRuntimeVersion != "" { + req.Header.Set("X-Stainless-Runtime-Version", fp.StainlessRuntimeVersion) + } +} + +// RewriteUserID 重写body中的metadata.user_id +// 输入格式:user_{clientId}_account__session_{sessionUUID} +// 输出格式:user_{cachedClientID}_account_{accountUUID}_session_{newHash} +func (s *IdentityService) RewriteUserID(body []byte, accountID int64, accountUUID, cachedClientID string) ([]byte, error) { + if len(body) == 0 || accountUUID == "" || cachedClientID == "" { + return body, nil + } + + // 解析JSON + var reqMap map[string]interface{} + if err := json.Unmarshal(body, &reqMap); err != nil { + return body, nil + } + + metadata, ok := reqMap["metadata"].(map[string]interface{}) + if !ok { + return body, nil + } + + userID, ok := metadata["user_id"].(string) + if !ok || userID == "" { + return body, nil + } + + // 匹配格式: user_{64位hex}_account__session_{uuid} + matches := userIDRegex.FindStringSubmatch(userID) + if matches == nil { + return body, nil + } + + sessionTail := matches[1] // 原始session UUID + + // 生成新的session hash: SHA256(accountID::sessionTail) -> UUID格式 + seed := fmt.Sprintf("%d::%s", accountID, sessionTail) + newSessionHash := generateUUIDFromSeed(seed) + + // 构建新的user_id + // 格式: user_{cachedClientID}_account_{account_uuid}_session_{newSessionHash} + newUserID := fmt.Sprintf("user_%s_account_%s_session_%s", cachedClientID, accountUUID, newSessionHash) + + metadata["user_id"] = newUserID + reqMap["metadata"] = metadata + + return json.Marshal(reqMap) +} + +// generateClientID 生成64位十六进制客户端ID(32字节随机数) +func generateClientID() string { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + // 极罕见的情况,使用时间戳+固定值作为fallback + log.Printf("Warning: crypto/rand.Read failed: %v, using fallback", err) + // 使用SHA256(当前纳秒时间)作为fallback + h := sha256.Sum256([]byte(fmt.Sprintf("%d", time.Now().UnixNano()))) + return hex.EncodeToString(h[:]) + } + return hex.EncodeToString(b) +} + +// generateUUIDFromSeed 从种子生成确定性UUID v4格式字符串 +func generateUUIDFromSeed(seed string) string { + hash := sha256.Sum256([]byte(seed)) + bytes := hash[:16] + + // 设置UUID v4版本和变体位 + bytes[6] = (bytes[6] & 0x0f) | 0x40 + bytes[8] = (bytes[8] & 0x3f) | 0x80 + + return fmt.Sprintf("%x-%x-%x-%x-%x", + bytes[0:4], bytes[4:6], bytes[6:8], bytes[8:10], bytes[10:16]) +} + +// parseUserAgentVersion 解析user-agent版本号 +// 例如:claude-cli/2.0.62 -> (2, 0, 62) +func parseUserAgentVersion(ua string) (major, minor, patch int, ok bool) { + // 匹配 xxx/x.y.z 格式 + matches := userAgentVersionRegex.FindStringSubmatch(ua) + if len(matches) != 4 { + return 0, 0, 0, false + } + major, _ = strconv.Atoi(matches[1]) + minor, _ = strconv.Atoi(matches[2]) + patch, _ = strconv.Atoi(matches[3]) + return major, minor, patch, true +} + +// isNewerVersion 比较版本号,判断newUA是否比cachedUA更新 +func isNewerVersion(newUA, cachedUA string) bool { + newMajor, newMinor, newPatch, newOk := parseUserAgentVersion(newUA) + cachedMajor, cachedMinor, cachedPatch, cachedOk := parseUserAgentVersion(cachedUA) + + if !newOk || !cachedOk { + return false + } + + // 比较版本号 + if newMajor > cachedMajor { + return true + } + if newMajor < cachedMajor { + return false + } + + if newMinor > cachedMinor { + return true + } + if newMinor < cachedMinor { + return false + } + + return newPatch > cachedPatch +} diff --git a/backend/internal/service/oauth_service.go b/backend/internal/service/oauth_service.go new file mode 100644 index 00000000..7e616fc4 --- /dev/null +++ b/backend/internal/service/oauth_service.go @@ -0,0 +1,471 @@ +package service + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "net/url" + "strings" + "time" + + "sub2api/internal/model" + "sub2api/internal/pkg/oauth" + "sub2api/internal/repository" + + "github.com/imroc/req/v3" +) + +// OAuthService handles OAuth authentication flows +type OAuthService struct { + sessionStore *oauth.SessionStore + proxyRepo *repository.ProxyRepository +} + +// NewOAuthService creates a new OAuth service +func NewOAuthService(proxyRepo *repository.ProxyRepository) *OAuthService { + return &OAuthService{ + sessionStore: oauth.NewSessionStore(), + proxyRepo: proxyRepo, + } +} + +// GenerateAuthURLResult contains the authorization URL and session info +type GenerateAuthURLResult struct { + AuthURL string `json:"auth_url"` + SessionID string `json:"session_id"` +} + +// GenerateAuthURL generates an OAuth authorization URL with full scope +func (s *OAuthService) GenerateAuthURL(ctx context.Context, proxyID *int64) (*GenerateAuthURLResult, error) { + scope := fmt.Sprintf("%s %s", oauth.ScopeProfile, oauth.ScopeInference) + return s.generateAuthURLWithScope(ctx, scope, proxyID) +} + +// GenerateSetupTokenURL generates an OAuth authorization URL for setup token (inference only) +func (s *OAuthService) GenerateSetupTokenURL(ctx context.Context, proxyID *int64) (*GenerateAuthURLResult, error) { + scope := oauth.ScopeInference + return s.generateAuthURLWithScope(ctx, scope, proxyID) +} + +func (s *OAuthService) generateAuthURLWithScope(ctx context.Context, scope string, proxyID *int64) (*GenerateAuthURLResult, error) { + // Generate PKCE values + state, err := oauth.GenerateState() + if err != nil { + return nil, fmt.Errorf("failed to generate state: %w", err) + } + + codeVerifier, err := oauth.GenerateCodeVerifier() + if err != nil { + return nil, fmt.Errorf("failed to generate code verifier: %w", err) + } + + codeChallenge := oauth.GenerateCodeChallenge(codeVerifier) + + // Generate session ID + sessionID, err := oauth.GenerateSessionID() + if err != nil { + return nil, fmt.Errorf("failed to generate session ID: %w", err) + } + + // Get proxy URL if specified + var proxyURL string + if proxyID != nil { + proxy, err := s.proxyRepo.GetByID(ctx, *proxyID) + if err == nil && proxy != nil { + proxyURL = proxy.URL() + } + } + + // Store session + session := &oauth.OAuthSession{ + State: state, + CodeVerifier: codeVerifier, + Scope: scope, + ProxyURL: proxyURL, + CreatedAt: time.Now(), + } + s.sessionStore.Set(sessionID, session) + + // Build authorization URL + authURL := oauth.BuildAuthorizationURL(state, codeChallenge, scope) + + return &GenerateAuthURLResult{ + AuthURL: authURL, + SessionID: sessionID, + }, nil +} + +// ExchangeCodeInput represents the input for code exchange +type ExchangeCodeInput struct { + SessionID string + Code string + ProxyID *int64 +} + +// TokenInfo represents the token information stored in credentials +type TokenInfo struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int64 `json:"expires_in"` + ExpiresAt int64 `json:"expires_at"` + RefreshToken string `json:"refresh_token,omitempty"` + Scope string `json:"scope,omitempty"` + OrgUUID string `json:"org_uuid,omitempty"` + AccountUUID string `json:"account_uuid,omitempty"` +} + +// ExchangeCode exchanges authorization code for tokens +func (s *OAuthService) ExchangeCode(ctx context.Context, input *ExchangeCodeInput) (*TokenInfo, error) { + // Get session + session, ok := s.sessionStore.Get(input.SessionID) + if !ok { + return nil, fmt.Errorf("session not found or expired") + } + + // Get proxy URL + proxyURL := session.ProxyURL + if input.ProxyID != nil { + proxy, err := s.proxyRepo.GetByID(ctx, *input.ProxyID) + if err == nil && proxy != nil { + proxyURL = proxy.URL() + } + } + + // Exchange code for token + tokenInfo, err := s.exchangeCodeForToken(ctx, input.Code, session.CodeVerifier, session.State, proxyURL) + if err != nil { + return nil, err + } + + // Delete session after successful exchange + s.sessionStore.Delete(input.SessionID) + + return tokenInfo, nil +} + +// CookieAuthInput represents the input for cookie-based authentication +type CookieAuthInput struct { + SessionKey string + ProxyID *int64 + Scope string // "full" or "inference" +} + +// CookieAuth performs OAuth using sessionKey (cookie-based auto-auth) +func (s *OAuthService) CookieAuth(ctx context.Context, input *CookieAuthInput) (*TokenInfo, error) { + // Get proxy URL if specified + var proxyURL string + if input.ProxyID != nil { + proxy, err := s.proxyRepo.GetByID(ctx, *input.ProxyID) + if err == nil && proxy != nil { + proxyURL = proxy.URL() + } + } + + // Determine scope + scope := fmt.Sprintf("%s %s", oauth.ScopeProfile, oauth.ScopeInference) + if input.Scope == "inference" { + scope = oauth.ScopeInference + } + + // Step 1: Get organization info using sessionKey + orgUUID, err := s.getOrganizationUUID(ctx, input.SessionKey, proxyURL) + if err != nil { + return nil, fmt.Errorf("failed to get organization info: %w", err) + } + + // Step 2: Generate PKCE values + codeVerifier, err := oauth.GenerateCodeVerifier() + if err != nil { + return nil, fmt.Errorf("failed to generate code verifier: %w", err) + } + codeChallenge := oauth.GenerateCodeChallenge(codeVerifier) + + state, err := oauth.GenerateState() + if err != nil { + return nil, fmt.Errorf("failed to generate state: %w", err) + } + + // Step 3: Get authorization code using cookie + authCode, err := s.getAuthorizationCode(ctx, input.SessionKey, orgUUID, scope, codeChallenge, state, proxyURL) + if err != nil { + return nil, fmt.Errorf("failed to get authorization code: %w", err) + } + + // Step 4: Exchange code for token + tokenInfo, err := s.exchangeCodeForToken(ctx, authCode, codeVerifier, state, proxyURL) + if err != nil { + return nil, fmt.Errorf("failed to exchange code: %w", err) + } + + // Ensure org_uuid is set (from step 1 if not from token response) + if tokenInfo.OrgUUID == "" && orgUUID != "" { + tokenInfo.OrgUUID = orgUUID + log.Printf("[OAuth] Set org_uuid from cookie auth: %s", orgUUID) + } + + return tokenInfo, nil +} + +// getOrganizationUUID gets the organization UUID from claude.ai using sessionKey +func (s *OAuthService) getOrganizationUUID(ctx context.Context, sessionKey, proxyURL string) (string, error) { + client := s.createReqClient(proxyURL) + + var orgs []struct { + UUID string `json:"uuid"` + } + + targetURL := "https://claude.ai/api/organizations" + log.Printf("[OAuth] Step 1: Getting organization UUID from %s", targetURL) + + resp, err := client.R(). + SetContext(ctx). + SetCookies(&http.Cookie{ + Name: "sessionKey", + Value: sessionKey, + }). + SetSuccessResult(&orgs). + Get(targetURL) + + if err != nil { + log.Printf("[OAuth] Step 1 FAILED - Request error: %v", err) + return "", fmt.Errorf("request failed: %w", err) + } + + log.Printf("[OAuth] Step 1 Response - Status: %d, Body: %s", resp.StatusCode, resp.String()) + + if !resp.IsSuccessState() { + return "", fmt.Errorf("failed to get organizations: status %d, body: %s", resp.StatusCode, resp.String()) + } + + if len(orgs) == 0 { + return "", fmt.Errorf("no organizations found") + } + + log.Printf("[OAuth] Step 1 SUCCESS - Got org UUID: %s", orgs[0].UUID) + return orgs[0].UUID, nil +} + +// getAuthorizationCode gets the authorization code using sessionKey +func (s *OAuthService) getAuthorizationCode(ctx context.Context, sessionKey, orgUUID, scope, codeChallenge, state, proxyURL string) (string, error) { + client := s.createReqClient(proxyURL) + + authURL := fmt.Sprintf("https://claude.ai/v1/oauth/%s/authorize", orgUUID) + + // Build request body - must include organization_uuid as per CRS + reqBody := map[string]interface{}{ + "response_type": "code", + "client_id": oauth.ClientID, + "organization_uuid": orgUUID, // Required field! + "redirect_uri": oauth.RedirectURI, + "scope": scope, + "state": state, + "code_challenge": codeChallenge, + "code_challenge_method": "S256", + } + + reqBodyJSON, _ := json.Marshal(reqBody) + log.Printf("[OAuth] Step 2: Getting authorization code from %s", authURL) + log.Printf("[OAuth] Step 2 Request Body: %s", string(reqBodyJSON)) + + // Response contains redirect_uri with code, not direct code field + var result struct { + RedirectURI string `json:"redirect_uri"` + } + + resp, err := client.R(). + SetContext(ctx). + SetCookies(&http.Cookie{ + Name: "sessionKey", + Value: sessionKey, + }). + SetHeader("Accept", "application/json"). + SetHeader("Accept-Language", "en-US,en;q=0.9"). + SetHeader("Cache-Control", "no-cache"). + SetHeader("Origin", "https://claude.ai"). + SetHeader("Referer", "https://claude.ai/new"). + SetHeader("Content-Type", "application/json"). + SetBody(reqBody). + SetSuccessResult(&result). + Post(authURL) + + if err != nil { + log.Printf("[OAuth] Step 2 FAILED - Request error: %v", err) + return "", fmt.Errorf("request failed: %w", err) + } + + log.Printf("[OAuth] Step 2 Response - Status: %d, Body: %s", resp.StatusCode, resp.String()) + + if !resp.IsSuccessState() { + return "", fmt.Errorf("failed to get authorization code: status %d, body: %s", resp.StatusCode, resp.String()) + } + + if result.RedirectURI == "" { + return "", fmt.Errorf("no redirect_uri in response") + } + + // Parse redirect_uri to extract code and state + parsedURL, err := url.Parse(result.RedirectURI) + if err != nil { + return "", fmt.Errorf("failed to parse redirect_uri: %w", err) + } + + queryParams := parsedURL.Query() + authCode := queryParams.Get("code") + responseState := queryParams.Get("state") + + if authCode == "" { + return "", fmt.Errorf("no authorization code in redirect_uri") + } + + // Combine code with state if present (as CRS does) + fullCode := authCode + if responseState != "" { + fullCode = authCode + "#" + responseState + } + + log.Printf("[OAuth] Step 2 SUCCESS - Got authorization code: %s...", authCode[:20]) + return fullCode, nil +} + +// exchangeCodeForToken exchanges authorization code for tokens +func (s *OAuthService) exchangeCodeForToken(ctx context.Context, code, codeVerifier, state, proxyURL string) (*TokenInfo, error) { + client := s.createReqClient(proxyURL) + + // Parse code#state format if present + authCode := code + codeState := "" + if parts := strings.Split(code, "#"); len(parts) > 1 { + authCode = parts[0] + codeState = parts[1] + } + + // Build JSON body as CRS does (not form data!) + reqBody := map[string]interface{}{ + "code": authCode, + "grant_type": "authorization_code", + "client_id": oauth.ClientID, + "redirect_uri": oauth.RedirectURI, + "code_verifier": codeVerifier, + } + + // Add state if present + if codeState != "" { + reqBody["state"] = codeState + } + + reqBodyJSON, _ := json.Marshal(reqBody) + log.Printf("[OAuth] Step 3: Exchanging code for token at %s", oauth.TokenURL) + log.Printf("[OAuth] Step 3 Request Body: %s", string(reqBodyJSON)) + + var tokenResp oauth.TokenResponse + + resp, err := client.R(). + SetContext(ctx). + SetHeader("Content-Type", "application/json"). + SetBody(reqBody). + SetSuccessResult(&tokenResp). + Post(oauth.TokenURL) + + if err != nil { + log.Printf("[OAuth] Step 3 FAILED - Request error: %v", err) + return nil, fmt.Errorf("request failed: %w", err) + } + + log.Printf("[OAuth] Step 3 Response - Status: %d, Body: %s", resp.StatusCode, resp.String()) + + if !resp.IsSuccessState() { + return nil, fmt.Errorf("token exchange failed: status %d, body: %s", resp.StatusCode, resp.String()) + } + + log.Printf("[OAuth] Step 3 SUCCESS - Got access token") + + tokenInfo := &TokenInfo{ + AccessToken: tokenResp.AccessToken, + TokenType: tokenResp.TokenType, + ExpiresIn: tokenResp.ExpiresIn, + ExpiresAt: time.Now().Unix() + tokenResp.ExpiresIn, + RefreshToken: tokenResp.RefreshToken, + Scope: tokenResp.Scope, + } + + // Extract org_uuid and account_uuid from response + if tokenResp.Organization != nil && tokenResp.Organization.UUID != "" { + tokenInfo.OrgUUID = tokenResp.Organization.UUID + log.Printf("[OAuth] Got org_uuid: %s", tokenInfo.OrgUUID) + } + if tokenResp.Account != nil && tokenResp.Account.UUID != "" { + tokenInfo.AccountUUID = tokenResp.Account.UUID + log.Printf("[OAuth] Got account_uuid: %s", tokenInfo.AccountUUID) + } + + return tokenInfo, nil +} + +// RefreshToken refreshes an OAuth token +func (s *OAuthService) RefreshToken(ctx context.Context, refreshToken string, proxyURL string) (*TokenInfo, error) { + client := s.createReqClient(proxyURL) + + formData := url.Values{} + formData.Set("grant_type", "refresh_token") + formData.Set("refresh_token", refreshToken) + formData.Set("client_id", oauth.ClientID) + + var tokenResp oauth.TokenResponse + + resp, err := client.R(). + SetContext(ctx). + SetFormDataFromValues(formData). + SetSuccessResult(&tokenResp). + Post(oauth.TokenURL) + + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + + if !resp.IsSuccessState() { + return nil, fmt.Errorf("token refresh failed: status %d, body: %s", resp.StatusCode, resp.String()) + } + + return &TokenInfo{ + AccessToken: tokenResp.AccessToken, + TokenType: tokenResp.TokenType, + ExpiresIn: tokenResp.ExpiresIn, + ExpiresAt: time.Now().Unix() + tokenResp.ExpiresIn, + RefreshToken: tokenResp.RefreshToken, + Scope: tokenResp.Scope, + }, nil +} + +// RefreshAccountToken refreshes token for an account +func (s *OAuthService) RefreshAccountToken(ctx context.Context, account *model.Account) (*TokenInfo, error) { + refreshToken := account.GetCredential("refresh_token") + if refreshToken == "" { + return nil, fmt.Errorf("no refresh token available") + } + + var proxyURL string + if account.ProxyID != nil { + proxy, err := s.proxyRepo.GetByID(ctx, *account.ProxyID) + if err == nil && proxy != nil { + proxyURL = proxy.URL() + } + } + + return s.RefreshToken(ctx, refreshToken, proxyURL) +} + +// createReqClient creates a req client with Chrome impersonation and optional proxy +func (s *OAuthService) createReqClient(proxyURL string) *req.Client { + client := req.C(). + ImpersonateChrome(). // Impersonate Chrome browser to bypass Cloudflare + SetTimeout(60 * time.Second) + + // Set proxy if specified + if proxyURL != "" { + client.SetProxyURL(proxyURL) + } + + return client +} diff --git a/backend/internal/service/pricing_service.go b/backend/internal/service/pricing_service.go new file mode 100644 index 00000000..d71908a5 --- /dev/null +++ b/backend/internal/service/pricing_service.go @@ -0,0 +1,572 @@ +package service + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "sub2api/internal/config" +) + +// LiteLLMModelPricing LiteLLM价格数据结构 +// 只保留我们需要的字段,使用指针来处理可能缺失的值 +type LiteLLMModelPricing struct { + InputCostPerToken float64 `json:"input_cost_per_token"` + OutputCostPerToken float64 `json:"output_cost_per_token"` + CacheCreationInputTokenCost float64 `json:"cache_creation_input_token_cost"` + CacheReadInputTokenCost float64 `json:"cache_read_input_token_cost"` + LiteLLMProvider string `json:"litellm_provider"` + Mode string `json:"mode"` + SupportsPromptCaching bool `json:"supports_prompt_caching"` +} + +// LiteLLMRawEntry 用于解析原始JSON数据 +type LiteLLMRawEntry struct { + InputCostPerToken *float64 `json:"input_cost_per_token"` + OutputCostPerToken *float64 `json:"output_cost_per_token"` + CacheCreationInputTokenCost *float64 `json:"cache_creation_input_token_cost"` + CacheReadInputTokenCost *float64 `json:"cache_read_input_token_cost"` + LiteLLMProvider string `json:"litellm_provider"` + Mode string `json:"mode"` + SupportsPromptCaching bool `json:"supports_prompt_caching"` +} + +// PricingService 动态价格服务 +type PricingService struct { + cfg *config.Config + mu sync.RWMutex + pricingData map[string]*LiteLLMModelPricing + lastUpdated time.Time + localHash string + + // 停止信号 + stopCh chan struct{} + wg sync.WaitGroup +} + +// NewPricingService 创建价格服务 +func NewPricingService(cfg *config.Config) *PricingService { + s := &PricingService{ + cfg: cfg, + pricingData: make(map[string]*LiteLLMModelPricing), + stopCh: make(chan struct{}), + } + return s +} + +// Initialize 初始化价格服务 +func (s *PricingService) Initialize() error { + // 确保数据目录存在 + if err := os.MkdirAll(s.cfg.Pricing.DataDir, 0755); err != nil { + log.Printf("[Pricing] Failed to create data directory: %v", err) + } + + // 首次加载价格数据 + if err := s.checkAndUpdatePricing(); err != nil { + log.Printf("[Pricing] Initial load failed, using fallback: %v", err) + if err := s.useFallbackPricing(); err != nil { + return fmt.Errorf("failed to load pricing data: %w", err) + } + } + + // 启动定时更新 + s.startUpdateScheduler() + + log.Printf("[Pricing] Service initialized with %d models", len(s.pricingData)) + return nil +} + +// Stop 停止价格服务 +func (s *PricingService) Stop() { + close(s.stopCh) + s.wg.Wait() + log.Println("[Pricing] Service stopped") +} + +// startUpdateScheduler 启动定时更新调度器 +func (s *PricingService) startUpdateScheduler() { + // 定期检查哈希更新 + hashInterval := time.Duration(s.cfg.Pricing.HashCheckIntervalMinutes) * time.Minute + if hashInterval < time.Minute { + hashInterval = 10 * time.Minute + } + + s.wg.Add(1) + go func() { + defer s.wg.Done() + ticker := time.NewTicker(hashInterval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + if err := s.syncWithRemote(); err != nil { + log.Printf("[Pricing] Sync failed: %v", err) + } + case <-s.stopCh: + return + } + } + }() + + log.Printf("[Pricing] Update scheduler started (check every %v)", hashInterval) +} + +// checkAndUpdatePricing 检查并更新价格数据 +func (s *PricingService) checkAndUpdatePricing() error { + pricingFile := s.getPricingFilePath() + + // 检查本地文件是否存在 + if _, err := os.Stat(pricingFile); os.IsNotExist(err) { + log.Println("[Pricing] Local pricing file not found, downloading...") + return s.downloadPricingData() + } + + // 检查文件是否过期 + info, err := os.Stat(pricingFile) + if err != nil { + return s.downloadPricingData() + } + + fileAge := time.Since(info.ModTime()) + maxAge := time.Duration(s.cfg.Pricing.UpdateIntervalHours) * time.Hour + + if fileAge > maxAge { + log.Printf("[Pricing] Local file is %v old, updating...", fileAge.Round(time.Hour)) + if err := s.downloadPricingData(); err != nil { + log.Printf("[Pricing] Download failed, using existing file: %v", err) + } + } + + // 加载本地文件 + return s.loadPricingData(pricingFile) +} + +// syncWithRemote 与远程同步(基于哈希校验) +func (s *PricingService) syncWithRemote() error { + pricingFile := s.getPricingFilePath() + + // 计算本地文件哈希 + localHash, err := s.computeFileHash(pricingFile) + if err != nil { + log.Printf("[Pricing] Failed to compute local hash: %v", err) + return s.downloadPricingData() + } + + // 如果配置了哈希URL,从远程获取哈希进行比对 + if s.cfg.Pricing.HashURL != "" { + remoteHash, err := s.fetchRemoteHash() + if err != nil { + log.Printf("[Pricing] Failed to fetch remote hash: %v", err) + return nil // 哈希获取失败不影响正常使用 + } + + if remoteHash != localHash { + log.Println("[Pricing] Remote hash differs, downloading new version...") + return s.downloadPricingData() + } + log.Println("[Pricing] Hash check passed, no update needed") + return nil + } + + // 没有哈希URL时,基于时间检查 + info, err := os.Stat(pricingFile) + if err != nil { + return s.downloadPricingData() + } + + fileAge := time.Since(info.ModTime()) + maxAge := time.Duration(s.cfg.Pricing.UpdateIntervalHours) * time.Hour + + if fileAge > maxAge { + log.Printf("[Pricing] File is %v old, downloading...", fileAge.Round(time.Hour)) + return s.downloadPricingData() + } + + return nil +} + +// downloadPricingData 从远程下载价格数据 +func (s *PricingService) downloadPricingData() error { + log.Printf("[Pricing] Downloading from %s", s.cfg.Pricing.RemoteURL) + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Get(s.cfg.Pricing.RemoteURL) + if err != nil { + return fmt.Errorf("download failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("download failed: HTTP %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("read response failed: %w", err) + } + + // 解析JSON数据(使用灵活的解析方式) + data, err := s.parsePricingData(body) + if err != nil { + return fmt.Errorf("parse pricing data: %w", err) + } + + // 保存到本地文件 + pricingFile := s.getPricingFilePath() + if err := os.WriteFile(pricingFile, body, 0644); err != nil { + log.Printf("[Pricing] Failed to save file: %v", err) + } + + // 保存哈希 + hash := sha256.Sum256(body) + hashStr := hex.EncodeToString(hash[:]) + hashFile := s.getHashFilePath() + if err := os.WriteFile(hashFile, []byte(hashStr+"\n"), 0644); err != nil { + log.Printf("[Pricing] Failed to save hash: %v", err) + } + + // 更新内存数据 + s.mu.Lock() + s.pricingData = data + s.lastUpdated = time.Now() + s.localHash = hashStr + s.mu.Unlock() + + log.Printf("[Pricing] Downloaded %d models successfully", len(data)) + return nil +} + +// parsePricingData 解析价格数据(处理各种格式) +func (s *PricingService) parsePricingData(body []byte) (map[string]*LiteLLMModelPricing, error) { + // 首先解析为 map[string]json.RawMessage + var rawData map[string]json.RawMessage + if err := json.Unmarshal(body, &rawData); err != nil { + return nil, fmt.Errorf("parse raw JSON: %w", err) + } + + result := make(map[string]*LiteLLMModelPricing) + skipped := 0 + + for modelName, rawEntry := range rawData { + // 跳过 sample_spec 等文档条目 + if modelName == "sample_spec" { + continue + } + + // 尝试解析每个条目 + var entry LiteLLMRawEntry + if err := json.Unmarshal(rawEntry, &entry); err != nil { + skipped++ + continue + } + + // 只保留有有效价格的条目 + if entry.InputCostPerToken == nil && entry.OutputCostPerToken == nil { + continue + } + + pricing := &LiteLLMModelPricing{ + LiteLLMProvider: entry.LiteLLMProvider, + Mode: entry.Mode, + SupportsPromptCaching: entry.SupportsPromptCaching, + } + + if entry.InputCostPerToken != nil { + pricing.InputCostPerToken = *entry.InputCostPerToken + } + if entry.OutputCostPerToken != nil { + pricing.OutputCostPerToken = *entry.OutputCostPerToken + } + if entry.CacheCreationInputTokenCost != nil { + pricing.CacheCreationInputTokenCost = *entry.CacheCreationInputTokenCost + } + if entry.CacheReadInputTokenCost != nil { + pricing.CacheReadInputTokenCost = *entry.CacheReadInputTokenCost + } + + result[modelName] = pricing + } + + if skipped > 0 { + log.Printf("[Pricing] Skipped %d invalid entries", skipped) + } + + if len(result) == 0 { + return nil, fmt.Errorf("no valid pricing entries found") + } + + return result, nil +} + +// loadPricingData 从本地文件加载价格数据 +func (s *PricingService) loadPricingData(filePath string) error { + data, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("read file failed: %w", err) + } + + // 使用灵活的解析方式 + pricingData, err := s.parsePricingData(data) + if err != nil { + return fmt.Errorf("parse pricing data: %w", err) + } + + // 计算哈希 + hash := sha256.Sum256(data) + hashStr := hex.EncodeToString(hash[:]) + + s.mu.Lock() + s.pricingData = pricingData + s.localHash = hashStr + + info, _ := os.Stat(filePath) + if info != nil { + s.lastUpdated = info.ModTime() + } else { + s.lastUpdated = time.Now() + } + s.mu.Unlock() + + log.Printf("[Pricing] Loaded %d models from %s", len(pricingData), filePath) + return nil +} + +// useFallbackPricing 使用回退价格文件 +func (s *PricingService) useFallbackPricing() error { + fallbackFile := s.cfg.Pricing.FallbackFile + + if _, err := os.Stat(fallbackFile); os.IsNotExist(err) { + return fmt.Errorf("fallback file not found: %s", fallbackFile) + } + + log.Printf("[Pricing] Using fallback file: %s", fallbackFile) + + // 复制到数据目录 + data, err := os.ReadFile(fallbackFile) + if err != nil { + return fmt.Errorf("read fallback failed: %w", err) + } + + pricingFile := s.getPricingFilePath() + if err := os.WriteFile(pricingFile, data, 0644); err != nil { + log.Printf("[Pricing] Failed to copy fallback: %v", err) + } + + return s.loadPricingData(fallbackFile) +} + +// fetchRemoteHash 从远程获取哈希值 +func (s *PricingService) fetchRemoteHash() (string, error) { + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Get(s.cfg.Pricing.HashURL) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("HTTP %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + // 哈希文件格式:hash filename 或者纯 hash + hash := strings.TrimSpace(string(body)) + parts := strings.Fields(hash) + if len(parts) > 0 { + return parts[0], nil + } + return hash, nil +} + +// computeFileHash 计算文件哈希 +func (s *PricingService) computeFileHash(filePath string) (string, error) { + data, err := os.ReadFile(filePath) + if err != nil { + return "", err + } + hash := sha256.Sum256(data) + return hex.EncodeToString(hash[:]), nil +} + +// GetModelPricing 获取模型价格(带模糊匹配) +func (s *PricingService) GetModelPricing(modelName string) *LiteLLMModelPricing { + s.mu.RLock() + defer s.mu.RUnlock() + + if modelName == "" { + return nil + } + + // 标准化模型名称 + modelLower := strings.ToLower(modelName) + + // 1. 精确匹配 + if pricing, ok := s.pricingData[modelLower]; ok { + return pricing + } + if pricing, ok := s.pricingData[modelName]; ok { + return pricing + } + + // 2. 处理常见的模型名称变体 + // claude-opus-4-5-20251101 -> claude-opus-4.5-20251101 + normalized := strings.ReplaceAll(modelLower, "-4-5-", "-4.5-") + if pricing, ok := s.pricingData[normalized]; ok { + return pricing + } + + // 3. 尝试模糊匹配(去掉版本号后缀) + // claude-opus-4-5-20251101 -> claude-opus-4.5 + baseName := s.extractBaseName(modelLower) + for key, pricing := range s.pricingData { + keyBase := s.extractBaseName(strings.ToLower(key)) + if keyBase == baseName { + return pricing + } + } + + // 4. 基于模型系列匹配 + return s.matchByModelFamily(modelLower) +} + +// extractBaseName 提取基础模型名称(去掉日期版本号) +func (s *PricingService) extractBaseName(model string) string { + // 移除日期后缀 (如 -20251101, -20241022) + parts := strings.Split(model, "-") + result := make([]string, 0, len(parts)) + for _, part := range parts { + // 跳过看起来像日期的部分(8位数字) + if len(part) == 8 && isNumeric(part) { + continue + } + // 跳过版本号(如 v1:0) + if strings.Contains(part, ":") { + continue + } + result = append(result, part) + } + return strings.Join(result, "-") +} + +// matchByModelFamily 基于模型系列匹配 +func (s *PricingService) matchByModelFamily(model string) *LiteLLMModelPricing { + // Claude模型系列匹配规则 + familyPatterns := map[string][]string{ + "opus-4.5": {"claude-opus-4.5", "claude-opus-4-5"}, + "opus-4": {"claude-opus-4", "claude-3-opus"}, + "sonnet-4.5": {"claude-sonnet-4.5", "claude-sonnet-4-5"}, + "sonnet-4": {"claude-sonnet-4", "claude-3-5-sonnet"}, + "sonnet-3.5": {"claude-3-5-sonnet", "claude-3.5-sonnet"}, + "sonnet-3": {"claude-3-sonnet"}, + "haiku-3.5": {"claude-3-5-haiku", "claude-3.5-haiku"}, + "haiku-3": {"claude-3-haiku"}, + } + + // 确定模型属于哪个系列 + var matchedFamily string + for family, patterns := range familyPatterns { + for _, pattern := range patterns { + if strings.Contains(model, pattern) || strings.Contains(model, strings.ReplaceAll(pattern, "-", "")) { + matchedFamily = family + break + } + } + if matchedFamily != "" { + break + } + } + + if matchedFamily == "" { + // 简单的系列匹配 + if strings.Contains(model, "opus") { + if strings.Contains(model, "4.5") || strings.Contains(model, "4-5") { + matchedFamily = "opus-4.5" + } else { + matchedFamily = "opus-4" + } + } else if strings.Contains(model, "sonnet") { + if strings.Contains(model, "4.5") || strings.Contains(model, "4-5") { + matchedFamily = "sonnet-4.5" + } else if strings.Contains(model, "3-5") || strings.Contains(model, "3.5") { + matchedFamily = "sonnet-3.5" + } else { + matchedFamily = "sonnet-4" + } + } else if strings.Contains(model, "haiku") { + if strings.Contains(model, "3-5") || strings.Contains(model, "3.5") { + matchedFamily = "haiku-3.5" + } else { + matchedFamily = "haiku-3" + } + } + } + + if matchedFamily == "" { + return nil + } + + // 在价格数据中查找该系列的模型 + patterns := familyPatterns[matchedFamily] + for _, pattern := range patterns { + for key, pricing := range s.pricingData { + keyLower := strings.ToLower(key) + if strings.Contains(keyLower, pattern) { + log.Printf("[Pricing] Fuzzy matched %s -> %s", model, key) + return pricing + } + } + } + + return nil +} + +// GetStatus 获取服务状态 +func (s *PricingService) GetStatus() map[string]interface{} { + s.mu.RLock() + defer s.mu.RUnlock() + + return map[string]interface{}{ + "model_count": len(s.pricingData), + "last_updated": s.lastUpdated, + "local_hash": s.localHash[:min(8, len(s.localHash))], + } +} + +// ForceUpdate 强制更新 +func (s *PricingService) ForceUpdate() error { + return s.downloadPricingData() +} + +// getPricingFilePath 获取价格文件路径 +func (s *PricingService) getPricingFilePath() string { + return filepath.Join(s.cfg.Pricing.DataDir, "model_pricing.json") +} + +// getHashFilePath 获取哈希文件路径 +func (s *PricingService) getHashFilePath() string { + return filepath.Join(s.cfg.Pricing.DataDir, "model_pricing.sha256") +} + +// isNumeric 检查字符串是否为纯数字 +func isNumeric(s string) bool { + for _, c := range s { + if c < '0' || c > '9' { + return false + } + } + return true +} diff --git a/backend/internal/service/proxy_service.go b/backend/internal/service/proxy_service.go new file mode 100644 index 00000000..92cba53a --- /dev/null +++ b/backend/internal/service/proxy_service.go @@ -0,0 +1,192 @@ +package service + +import ( + "context" + "errors" + "fmt" + "sub2api/internal/model" + "sub2api/internal/repository" + + "gorm.io/gorm" +) + +var ( + ErrProxyNotFound = errors.New("proxy not found") +) + +// CreateProxyRequest 创建代理请求 +type CreateProxyRequest struct { + Name string `json:"name"` + Protocol string `json:"protocol"` + Host string `json:"host"` + Port int `json:"port"` + Username string `json:"username"` + Password string `json:"password"` +} + +// UpdateProxyRequest 更新代理请求 +type UpdateProxyRequest struct { + Name *string `json:"name"` + Protocol *string `json:"protocol"` + Host *string `json:"host"` + Port *int `json:"port"` + Username *string `json:"username"` + Password *string `json:"password"` + Status *string `json:"status"` +} + +// ProxyService 代理管理服务 +type ProxyService struct { + proxyRepo *repository.ProxyRepository +} + +// NewProxyService 创建代理服务实例 +func NewProxyService(proxyRepo *repository.ProxyRepository) *ProxyService { + return &ProxyService{ + proxyRepo: proxyRepo, + } +} + +// Create 创建代理 +func (s *ProxyService) Create(ctx context.Context, req CreateProxyRequest) (*model.Proxy, error) { + // 创建代理 + proxy := &model.Proxy{ + Name: req.Name, + Protocol: req.Protocol, + Host: req.Host, + Port: req.Port, + Username: req.Username, + Password: req.Password, + Status: model.StatusActive, + } + + if err := s.proxyRepo.Create(ctx, proxy); err != nil { + return nil, fmt.Errorf("create proxy: %w", err) + } + + return proxy, nil +} + +// GetByID 根据ID获取代理 +func (s *ProxyService) GetByID(ctx context.Context, id int64) (*model.Proxy, error) { + proxy, err := s.proxyRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrProxyNotFound + } + return nil, fmt.Errorf("get proxy: %w", err) + } + return proxy, nil +} + +// List 获取代理列表 +func (s *ProxyService) List(ctx context.Context, params repository.PaginationParams) ([]model.Proxy, *repository.PaginationResult, error) { + proxies, pagination, err := s.proxyRepo.List(ctx, params) + if err != nil { + return nil, nil, fmt.Errorf("list proxies: %w", err) + } + return proxies, pagination, nil +} + +// ListActive 获取活跃代理列表 +func (s *ProxyService) ListActive(ctx context.Context) ([]model.Proxy, error) { + proxies, err := s.proxyRepo.ListActive(ctx) + if err != nil { + return nil, fmt.Errorf("list active proxies: %w", err) + } + return proxies, nil +} + +// Update 更新代理 +func (s *ProxyService) Update(ctx context.Context, id int64, req UpdateProxyRequest) (*model.Proxy, error) { + proxy, err := s.proxyRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrProxyNotFound + } + return nil, fmt.Errorf("get proxy: %w", err) + } + + // 更新字段 + if req.Name != nil { + proxy.Name = *req.Name + } + + if req.Protocol != nil { + proxy.Protocol = *req.Protocol + } + + if req.Host != nil { + proxy.Host = *req.Host + } + + if req.Port != nil { + proxy.Port = *req.Port + } + + if req.Username != nil { + proxy.Username = *req.Username + } + + if req.Password != nil { + proxy.Password = *req.Password + } + + if req.Status != nil { + proxy.Status = *req.Status + } + + if err := s.proxyRepo.Update(ctx, proxy); err != nil { + return nil, fmt.Errorf("update proxy: %w", err) + } + + return proxy, nil +} + +// Delete 删除代理 +func (s *ProxyService) Delete(ctx context.Context, id int64) error { + // 检查代理是否存在 + _, err := s.proxyRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrProxyNotFound + } + return fmt.Errorf("get proxy: %w", err) + } + + if err := s.proxyRepo.Delete(ctx, id); err != nil { + return fmt.Errorf("delete proxy: %w", err) + } + + return nil +} + +// TestConnection 测试代理连接(需要实现具体测试逻辑) +func (s *ProxyService) TestConnection(ctx context.Context, id int64) error { + proxy, err := s.proxyRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrProxyNotFound + } + return fmt.Errorf("get proxy: %w", err) + } + + // TODO: 实现代理连接测试逻辑 + // 可以尝试通过代理发送测试请求 + _ = proxy + + return nil +} + +// GetURL 获取代理URL +func (s *ProxyService) GetURL(ctx context.Context, id int64) (string, error) { + proxy, err := s.proxyRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return "", ErrProxyNotFound + } + return "", fmt.Errorf("get proxy: %w", err) + } + + return proxy.URL(), nil +} diff --git a/backend/internal/service/ratelimit_service.go b/backend/internal/service/ratelimit_service.go new file mode 100644 index 00000000..43feef30 --- /dev/null +++ b/backend/internal/service/ratelimit_service.go @@ -0,0 +1,170 @@ +package service + +import ( + "context" + "log" + "net/http" + "strconv" + "time" + + "sub2api/internal/config" + "sub2api/internal/model" + "sub2api/internal/repository" +) + +// RateLimitService 处理限流和过载状态管理 +type RateLimitService struct { + repos *repository.Repositories + cfg *config.Config +} + +// NewRateLimitService 创建RateLimitService实例 +func NewRateLimitService(repos *repository.Repositories, cfg *config.Config) *RateLimitService { + return &RateLimitService{ + repos: repos, + cfg: cfg, + } +} + +// HandleUpstreamError 处理上游错误响应,标记账号状态 +// 返回是否应该停止该账号的调度 +func (s *RateLimitService) HandleUpstreamError(ctx context.Context, account *model.Account, statusCode int, headers http.Header, responseBody []byte) (shouldDisable bool) { + // apikey 类型账号:检查自定义错误码配置 + // 如果启用且错误码不在列表中,则不处理(不停止调度、不标记限流/过载) + if !account.ShouldHandleErrorCode(statusCode) { + log.Printf("Account %d: error %d skipped (not in custom error codes)", account.ID, statusCode) + return false + } + + switch statusCode { + case 401: + // 认证失败:停止调度,记录错误 + s.handleAuthError(ctx, account, "Authentication failed (401): invalid or expired credentials") + return true + case 403: + // 禁止访问:停止调度,记录错误 + s.handleAuthError(ctx, account, "Access forbidden (403): account may be suspended or lack permissions") + return true + case 429: + s.handle429(ctx, account, headers) + return false + case 529: + s.handle529(ctx, account) + return false + default: + // 其他5xx错误:记录但不停止调度 + if statusCode >= 500 { + log.Printf("Account %d received upstream error %d", account.ID, statusCode) + } + return false + } +} + +// handleAuthError 处理认证类错误(401/403),停止账号调度 +func (s *RateLimitService) handleAuthError(ctx context.Context, account *model.Account, errorMsg string) { + if err := s.repos.Account.SetError(ctx, account.ID, errorMsg); err != nil { + log.Printf("SetError failed for account %d: %v", account.ID, err) + return + } + log.Printf("Account %d disabled due to auth error: %s", account.ID, errorMsg) +} + +// handle429 处理429限流错误 +// 解析响应头获取重置时间,标记账号为限流状态 +func (s *RateLimitService) handle429(ctx context.Context, account *model.Account, headers http.Header) { + // 解析重置时间戳 + resetTimestamp := headers.Get("anthropic-ratelimit-unified-reset") + if resetTimestamp == "" { + // 没有重置时间,使用默认5分钟 + resetAt := time.Now().Add(5 * time.Minute) + if err := s.repos.Account.SetRateLimited(ctx, account.ID, resetAt); err != nil { + log.Printf("SetRateLimited failed for account %d: %v", account.ID, err) + } + return + } + + // 解析Unix时间戳 + ts, err := strconv.ParseInt(resetTimestamp, 10, 64) + if err != nil { + log.Printf("Parse reset timestamp failed: %v", err) + resetAt := time.Now().Add(5 * time.Minute) + if err := s.repos.Account.SetRateLimited(ctx, account.ID, resetAt); err != nil { + log.Printf("SetRateLimited failed for account %d: %v", account.ID, err) + } + return + } + + resetAt := time.Unix(ts, 0) + + // 标记限流状态 + if err := s.repos.Account.SetRateLimited(ctx, account.ID, resetAt); err != nil { + log.Printf("SetRateLimited failed for account %d: %v", account.ID, err) + return + } + + // 根据重置时间反推5h窗口 + windowEnd := resetAt + windowStart := resetAt.Add(-5 * time.Hour) + if err := s.repos.Account.UpdateSessionWindow(ctx, account.ID, &windowStart, &windowEnd, "rejected"); err != nil { + log.Printf("UpdateSessionWindow failed for account %d: %v", account.ID, err) + } + + log.Printf("Account %d rate limited until %v", account.ID, resetAt) +} + +// handle529 处理529过载错误 +// 根据配置设置过载冷却时间 +func (s *RateLimitService) handle529(ctx context.Context, account *model.Account) { + cooldownMinutes := s.cfg.RateLimit.OverloadCooldownMinutes + if cooldownMinutes <= 0 { + cooldownMinutes = 10 // 默认10分钟 + } + + until := time.Now().Add(time.Duration(cooldownMinutes) * time.Minute) + if err := s.repos.Account.SetOverloaded(ctx, account.ID, until); err != nil { + log.Printf("SetOverloaded failed for account %d: %v", account.ID, err) + return + } + + log.Printf("Account %d overloaded until %v", account.ID, until) +} + +// UpdateSessionWindow 从成功响应更新5h窗口状态 +func (s *RateLimitService) UpdateSessionWindow(ctx context.Context, account *model.Account, headers http.Header) { + status := headers.Get("anthropic-ratelimit-unified-5h-status") + if status == "" { + return + } + + // 检查是否需要初始化时间窗口 + // 对于 Setup Token 账号,首次成功请求时需要预测时间窗口 + var windowStart, windowEnd *time.Time + needInitWindow := account.SessionWindowEnd == nil || time.Now().After(*account.SessionWindowEnd) + + if needInitWindow && (status == "allowed" || status == "allowed_warning") { + // 预测时间窗口:从当前时间的整点开始,+5小时为结束 + // 例如:现在是 14:30,窗口为 14:00 ~ 19:00 + now := time.Now() + start := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location()) + end := start.Add(5 * time.Hour) + windowStart = &start + windowEnd = &end + log.Printf("Account %d: initializing 5h window from %v to %v (status: %s)", account.ID, start, end, status) + } + + if err := s.repos.Account.UpdateSessionWindow(ctx, account.ID, windowStart, windowEnd, status); err != nil { + log.Printf("UpdateSessionWindow failed for account %d: %v", account.ID, err) + } + + // 如果状态为allowed且之前有限流,说明窗口已重置,清除限流状态 + if status == "allowed" && account.IsRateLimited() { + if err := s.repos.Account.ClearRateLimit(ctx, account.ID); err != nil { + log.Printf("ClearRateLimit failed for account %d: %v", account.ID, err) + } + } +} + +// ClearRateLimit 清除账号的限流状态 +func (s *RateLimitService) ClearRateLimit(ctx context.Context, accountID int64) error { + return s.repos.Account.ClearRateLimit(ctx, accountID) +} diff --git a/backend/internal/service/redeem_service.go b/backend/internal/service/redeem_service.go new file mode 100644 index 00000000..5601a9c1 --- /dev/null +++ b/backend/internal/service/redeem_service.go @@ -0,0 +1,392 @@ +package service + +import ( + "context" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "strings" + "sub2api/internal/model" + "sub2api/internal/repository" + "time" + + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +var ( + ErrRedeemCodeNotFound = errors.New("redeem code not found") + ErrRedeemCodeUsed = errors.New("redeem code already used") + ErrRedeemCodeInvalid = errors.New("invalid redeem code") + ErrInsufficientBalance = errors.New("insufficient balance") + ErrRedeemRateLimited = errors.New("too many failed attempts, please try again later") + ErrRedeemCodeLocked = errors.New("redeem code is being processed, please try again") +) + +const ( + redeemRateLimitKeyPrefix = "redeem:rate_limit:" + redeemLockKeyPrefix = "redeem:lock:" + redeemMaxErrorsPerHour = 20 + redeemRateLimitDuration = time.Hour + redeemLockDuration = 10 * time.Second // 锁超时时间,防止死锁 +) + +// GenerateCodesRequest 生成兑换码请求 +type GenerateCodesRequest struct { + Count int `json:"count"` + Value float64 `json:"value"` + Type string `json:"type"` +} + +// RedeemCodeResponse 兑换码响应 +type RedeemCodeResponse struct { + Code string `json:"code"` + Value float64 `json:"value"` + Status string `json:"status"` + CreatedAt time.Time `json:"created_at"` +} + +// RedeemService 兑换码服务 +type RedeemService struct { + redeemRepo *repository.RedeemCodeRepository + userRepo *repository.UserRepository + subscriptionService *SubscriptionService + rdb *redis.Client + billingCacheService *BillingCacheService +} + +// NewRedeemService 创建兑换码服务实例 +func NewRedeemService(redeemRepo *repository.RedeemCodeRepository, userRepo *repository.UserRepository, subscriptionService *SubscriptionService, rdb *redis.Client) *RedeemService { + return &RedeemService{ + redeemRepo: redeemRepo, + userRepo: userRepo, + subscriptionService: subscriptionService, + rdb: rdb, + } +} + +// SetBillingCacheService 设置计费缓存服务(用于缓存失效) +func (s *RedeemService) SetBillingCacheService(billingCacheService *BillingCacheService) { + s.billingCacheService = billingCacheService +} + +// GenerateRandomCode 生成随机兑换码 +func (s *RedeemService) GenerateRandomCode() (string, error) { + // 生成16字节随机数据 + bytes := make([]byte, 16) + if _, err := rand.Read(bytes); err != nil { + return "", fmt.Errorf("generate random bytes: %w", err) + } + + // 转换为十六进制字符串 + code := hex.EncodeToString(bytes) + + // 格式化为 XXXX-XXXX-XXXX-XXXX 格式 + parts := []string{ + strings.ToUpper(code[0:8]), + strings.ToUpper(code[8:16]), + strings.ToUpper(code[16:24]), + strings.ToUpper(code[24:32]), + } + + return strings.Join(parts, "-"), nil +} + +// GenerateCodes 批量生成兑换码 +func (s *RedeemService) GenerateCodes(ctx context.Context, req GenerateCodesRequest) ([]model.RedeemCode, error) { + if req.Count <= 0 { + return nil, errors.New("count must be greater than 0") + } + + if req.Value <= 0 { + return nil, errors.New("value must be greater than 0") + } + + if req.Count > 1000 { + return nil, errors.New("cannot generate more than 1000 codes at once") + } + + codeType := req.Type + if codeType == "" { + codeType = model.RedeemTypeBalance + } + + codes := make([]model.RedeemCode, 0, req.Count) + for i := 0; i < req.Count; i++ { + code, err := s.GenerateRandomCode() + if err != nil { + return nil, fmt.Errorf("generate code: %w", err) + } + + codes = append(codes, model.RedeemCode{ + Code: code, + Type: codeType, + Value: req.Value, + Status: model.StatusUnused, + }) + } + + // 批量插入 + if err := s.redeemRepo.CreateBatch(ctx, codes); err != nil { + return nil, fmt.Errorf("create batch codes: %w", err) + } + + return codes, nil +} + +// checkRedeemRateLimit 检查用户兑换错误次数是否超限 +func (s *RedeemService) checkRedeemRateLimit(ctx context.Context, userID int64) error { + if s.rdb == nil { + return nil + } + + key := fmt.Sprintf("%s%d", redeemRateLimitKeyPrefix, userID) + + count, err := s.rdb.Get(ctx, key).Int() + if err != nil && !errors.Is(err, redis.Nil) { + // Redis 出错时不阻止用户操作 + return nil + } + + if count >= redeemMaxErrorsPerHour { + return ErrRedeemRateLimited + } + + return nil +} + +// incrementRedeemErrorCount 增加用户兑换错误计数 +func (s *RedeemService) incrementRedeemErrorCount(ctx context.Context, userID int64) { + if s.rdb == nil { + return + } + + key := fmt.Sprintf("%s%d", redeemRateLimitKeyPrefix, userID) + + pipe := s.rdb.Pipeline() + pipe.Incr(ctx, key) + pipe.Expire(ctx, key, redeemRateLimitDuration) + _, _ = pipe.Exec(ctx) +} + +// acquireRedeemLock 尝试获取兑换码的分布式锁 +// 返回 true 表示获取成功,false 表示锁已被占用 +func (s *RedeemService) acquireRedeemLock(ctx context.Context, code string) bool { + if s.rdb == nil { + return true // 无 Redis 时降级为不加锁 + } + + key := redeemLockKeyPrefix + code + ok, err := s.rdb.SetNX(ctx, key, "1", redeemLockDuration).Result() + if err != nil { + // Redis 出错时不阻止操作,依赖数据库层面的状态检查 + return true + } + return ok +} + +// releaseRedeemLock 释放兑换码的分布式锁 +func (s *RedeemService) releaseRedeemLock(ctx context.Context, code string) { + if s.rdb == nil { + return + } + + key := redeemLockKeyPrefix + code + s.rdb.Del(ctx, key) +} + +// Redeem 使用兑换码 +func (s *RedeemService) Redeem(ctx context.Context, userID int64, code string) (*model.RedeemCode, error) { + // 检查限流 + if err := s.checkRedeemRateLimit(ctx, userID); err != nil { + return nil, err + } + + // 获取分布式锁,防止同一兑换码并发使用 + if !s.acquireRedeemLock(ctx, code) { + return nil, ErrRedeemCodeLocked + } + defer s.releaseRedeemLock(ctx, code) + + // 查找兑换码 + redeemCode, err := s.redeemRepo.GetByCode(ctx, code) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + s.incrementRedeemErrorCount(ctx, userID) + return nil, ErrRedeemCodeNotFound + } + return nil, fmt.Errorf("get redeem code: %w", err) + } + + // 检查兑换码状态 + if !redeemCode.CanUse() { + s.incrementRedeemErrorCount(ctx, userID) + return nil, ErrRedeemCodeUsed + } + + // 验证兑换码类型的前置条件 + if redeemCode.Type == model.RedeemTypeSubscription && redeemCode.GroupID == nil { + return nil, errors.New("invalid subscription redeem code: missing group_id") + } + + // 获取用户信息 + user, err := s.userRepo.GetByID(ctx, userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrUserNotFound + } + return nil, fmt.Errorf("get user: %w", err) + } + _ = user // 使用变量避免未使用错误 + + // 【关键】先标记兑换码为已使用,确保并发安全 + // 利用数据库乐观锁(WHERE status = 'unused')保证原子性 + if err := s.redeemRepo.Use(ctx, redeemCode.ID, userID); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // 兑换码已被其他请求使用 + return nil, ErrRedeemCodeUsed + } + return nil, fmt.Errorf("mark code as used: %w", err) + } + + // 执行兑换逻辑(兑换码已被锁定,此时可安全操作) + switch redeemCode.Type { + case model.RedeemTypeBalance: + // 增加用户余额 + if err := s.userRepo.UpdateBalance(ctx, userID, redeemCode.Value); err != nil { + return nil, fmt.Errorf("update user balance: %w", err) + } + // 失效余额缓存 + if s.billingCacheService != nil { + go func() { + cacheCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + s.billingCacheService.InvalidateUserBalance(cacheCtx, userID) + }() + } + + case model.RedeemTypeConcurrency: + // 增加用户并发数 + if err := s.userRepo.UpdateConcurrency(ctx, userID, int(redeemCode.Value)); err != nil { + return nil, fmt.Errorf("update user concurrency: %w", err) + } + + case model.RedeemTypeSubscription: + validityDays := redeemCode.ValidityDays + if validityDays <= 0 { + validityDays = 30 + } + _, _, err := s.subscriptionService.AssignOrExtendSubscription(ctx, &AssignSubscriptionInput{ + UserID: userID, + GroupID: *redeemCode.GroupID, + ValidityDays: validityDays, + AssignedBy: 0, // 系统分配 + Notes: fmt.Sprintf("通过兑换码 %s 兑换", redeemCode.Code), + }) + if err != nil { + return nil, fmt.Errorf("assign or extend subscription: %w", err) + } + // 失效订阅缓存 + if s.billingCacheService != nil { + groupID := *redeemCode.GroupID + go func() { + cacheCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + s.billingCacheService.InvalidateSubscription(cacheCtx, userID, groupID) + }() + } + + default: + return nil, fmt.Errorf("unsupported redeem type: %s", redeemCode.Type) + } + + // 重新获取更新后的兑换码 + redeemCode, err = s.redeemRepo.GetByID(ctx, redeemCode.ID) + if err != nil { + return nil, fmt.Errorf("get updated redeem code: %w", err) + } + + return redeemCode, nil +} + +// GetByID 根据ID获取兑换码 +func (s *RedeemService) GetByID(ctx context.Context, id int64) (*model.RedeemCode, error) { + code, err := s.redeemRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrRedeemCodeNotFound + } + return nil, fmt.Errorf("get redeem code: %w", err) + } + return code, nil +} + +// GetByCode 根据Code获取兑换码 +func (s *RedeemService) GetByCode(ctx context.Context, code string) (*model.RedeemCode, error) { + redeemCode, err := s.redeemRepo.GetByCode(ctx, code) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrRedeemCodeNotFound + } + return nil, fmt.Errorf("get redeem code: %w", err) + } + return redeemCode, nil +} + +// List 获取兑换码列表(管理员功能) +func (s *RedeemService) List(ctx context.Context, params repository.PaginationParams) ([]model.RedeemCode, *repository.PaginationResult, error) { + codes, pagination, err := s.redeemRepo.List(ctx, params) + if err != nil { + return nil, nil, fmt.Errorf("list redeem codes: %w", err) + } + return codes, pagination, nil +} + +// Delete 删除兑换码(管理员功能) +func (s *RedeemService) Delete(ctx context.Context, id int64) error { + // 检查兑换码是否存在 + code, err := s.redeemRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrRedeemCodeNotFound + } + return fmt.Errorf("get redeem code: %w", err) + } + + // 不允许删除已使用的兑换码 + if code.IsUsed() { + return errors.New("cannot delete used redeem code") + } + + if err := s.redeemRepo.Delete(ctx, id); err != nil { + return fmt.Errorf("delete redeem code: %w", err) + } + + return nil +} + +// GetStats 获取兑换码统计信息 +func (s *RedeemService) GetStats(ctx context.Context) (map[string]interface{}, error) { + // TODO: 实现统计逻辑 + // 统计未使用、已使用的兑换码数量 + // 统计总面值等 + + stats := map[string]interface{}{ + "total_codes": 0, + "unused_codes": 0, + "used_codes": 0, + "total_value": 0.0, + } + + return stats, nil +} + +// GetUserHistory 获取用户的兑换历史 +func (s *RedeemService) GetUserHistory(ctx context.Context, userID int64, limit int) ([]model.RedeemCode, error) { + codes, err := s.redeemRepo.ListByUser(ctx, userID, limit) + if err != nil { + return nil, fmt.Errorf("get user redeem history: %w", err) + } + return codes, nil +} diff --git a/backend/internal/service/service.go b/backend/internal/service/service.go new file mode 100644 index 00000000..dcb964b9 --- /dev/null +++ b/backend/internal/service/service.go @@ -0,0 +1,139 @@ +package service + +import ( + "sub2api/internal/config" + "sub2api/internal/repository" + + "github.com/redis/go-redis/v9" +) + +// Services 服务集合容器 +type Services struct { + Auth *AuthService + User *UserService + ApiKey *ApiKeyService + Group *GroupService + Account *AccountService + Proxy *ProxyService + Redeem *RedeemService + Usage *UsageService + Pricing *PricingService + Billing *BillingService + BillingCache *BillingCacheService + Admin AdminService + Gateway *GatewayService + OAuth *OAuthService + RateLimit *RateLimitService + AccountUsage *AccountUsageService + AccountTest *AccountTestService + Setting *SettingService + Email *EmailService + EmailQueue *EmailQueueService + Turnstile *TurnstileService + Subscription *SubscriptionService + Concurrency *ConcurrencyService + Identity *IdentityService +} + +// NewServices 创建所有服务实例 +func NewServices(repos *repository.Repositories, rdb *redis.Client, cfg *config.Config) *Services { + // 初始化价格服务 + pricingService := NewPricingService(cfg) + if err := pricingService.Initialize(); err != nil { + // 价格服务初始化失败不应阻止启动,使用回退价格 + println("[Service] Warning: Pricing service initialization failed:", err.Error()) + } + + // 初始化计费服务(依赖价格服务) + billingService := NewBillingService(cfg, pricingService) + + // 初始化其他服务 + authService := NewAuthService(repos.User, cfg) + userService := NewUserService(repos.User, cfg) + apiKeyService := NewApiKeyService(repos.ApiKey, repos.User, repos.Group, repos.UserSubscription, rdb, cfg) + groupService := NewGroupService(repos.Group) + accountService := NewAccountService(repos.Account, repos.Group) + proxyService := NewProxyService(repos.Proxy) + usageService := NewUsageService(repos.UsageLog, repos.User) + + // 初始化订阅服务 (RedeemService 依赖) + subscriptionService := NewSubscriptionService(repos) + + // 初始化兑换服务 (依赖订阅服务) + redeemService := NewRedeemService(repos.RedeemCode, repos.User, subscriptionService, rdb) + + // 初始化Admin服务 + adminService := NewAdminService(repos) + + // 初始化OAuth服务(GatewayService依赖) + oauthService := NewOAuthService(repos.Proxy) + + // 初始化限流服务 + rateLimitService := NewRateLimitService(repos, cfg) + + // 初始化计费缓存服务 + billingCacheService := NewBillingCacheService(rdb, repos.User, repos.UserSubscription) + + // 初始化账号使用量服务 + accountUsageService := NewAccountUsageService(repos, oauthService) + + // 初始化账号测试服务 + accountTestService := NewAccountTestService(repos, oauthService) + + // 初始化身份指纹服务 + identityService := NewIdentityService(rdb) + + // 初始化Gateway服务 + gatewayService := NewGatewayService(repos, rdb, cfg, oauthService, billingService, rateLimitService, billingCacheService, identityService) + + // 初始化设置服务 + settingService := NewSettingService(repos.Setting, cfg) + emailService := NewEmailService(repos.Setting, rdb) + + // 初始化邮件队列服务 + emailQueueService := NewEmailQueueService(emailService, 3) + + // 初始化Turnstile服务 + turnstileService := NewTurnstileService(settingService) + + // 设置Auth服务的依赖(用于注册开关和邮件验证) + authService.SetSettingService(settingService) + authService.SetEmailService(emailService) + authService.SetTurnstileService(turnstileService) + authService.SetEmailQueueService(emailQueueService) + + // 初始化并发控制服务 + concurrencyService := NewConcurrencyService(rdb) + + // 注入计费缓存服务到需要失效缓存的服务 + redeemService.SetBillingCacheService(billingCacheService) + subscriptionService.SetBillingCacheService(billingCacheService) + SetAdminServiceBillingCache(adminService, billingCacheService) + + return &Services{ + Auth: authService, + User: userService, + ApiKey: apiKeyService, + Group: groupService, + Account: accountService, + Proxy: proxyService, + Redeem: redeemService, + Usage: usageService, + Pricing: pricingService, + Billing: billingService, + BillingCache: billingCacheService, + Admin: adminService, + Gateway: gatewayService, + OAuth: oauthService, + RateLimit: rateLimitService, + AccountUsage: accountUsageService, + AccountTest: accountTestService, + Setting: settingService, + Email: emailService, + EmailQueue: emailQueueService, + Turnstile: turnstileService, + Subscription: subscriptionService, + Concurrency: concurrencyService, + Identity: identityService, + } +} diff --git a/backend/internal/service/setting_service.go b/backend/internal/service/setting_service.go new file mode 100644 index 00000000..013b07f6 --- /dev/null +++ b/backend/internal/service/setting_service.go @@ -0,0 +1,264 @@ +package service + +import ( + "context" + "errors" + "fmt" + "strconv" + "sub2api/internal/config" + "sub2api/internal/model" + "sub2api/internal/repository" + + "gorm.io/gorm" +) + +var ( + ErrRegistrationDisabled = errors.New("registration is currently disabled") +) + +// SettingService 系统设置服务 +type SettingService struct { + settingRepo *repository.SettingRepository + cfg *config.Config +} + +// NewSettingService 创建系统设置服务实例 +func NewSettingService(settingRepo *repository.SettingRepository, cfg *config.Config) *SettingService { + return &SettingService{ + settingRepo: settingRepo, + cfg: cfg, + } +} + +// GetAllSettings 获取所有系统设置 +func (s *SettingService) GetAllSettings(ctx context.Context) (*model.SystemSettings, error) { + settings, err := s.settingRepo.GetAll(ctx) + if err != nil { + return nil, fmt.Errorf("get all settings: %w", err) + } + + return s.parseSettings(settings), nil +} + +// GetPublicSettings 获取公开设置(无需登录) +func (s *SettingService) GetPublicSettings(ctx context.Context) (*model.PublicSettings, error) { + keys := []string{ + model.SettingKeyRegistrationEnabled, + model.SettingKeyEmailVerifyEnabled, + model.SettingKeyTurnstileEnabled, + model.SettingKeyTurnstileSiteKey, + model.SettingKeySiteName, + model.SettingKeySiteLogo, + model.SettingKeySiteSubtitle, + model.SettingKeyApiBaseUrl, + model.SettingKeyContactInfo, + } + + settings, err := s.settingRepo.GetMultiple(ctx, keys) + if err != nil { + return nil, fmt.Errorf("get public settings: %w", err) + } + + return &model.PublicSettings{ + RegistrationEnabled: settings[model.SettingKeyRegistrationEnabled] == "true", + EmailVerifyEnabled: settings[model.SettingKeyEmailVerifyEnabled] == "true", + TurnstileEnabled: settings[model.SettingKeyTurnstileEnabled] == "true", + TurnstileSiteKey: settings[model.SettingKeyTurnstileSiteKey], + SiteName: s.getStringOrDefault(settings, model.SettingKeySiteName, "Sub2API"), + SiteLogo: settings[model.SettingKeySiteLogo], + SiteSubtitle: s.getStringOrDefault(settings, model.SettingKeySiteSubtitle, "Subscription to API Conversion Platform"), + ApiBaseUrl: settings[model.SettingKeyApiBaseUrl], + ContactInfo: settings[model.SettingKeyContactInfo], + }, nil +} + +// UpdateSettings 更新系统设置 +func (s *SettingService) UpdateSettings(ctx context.Context, settings *model.SystemSettings) error { + updates := make(map[string]string) + + // 注册设置 + updates[model.SettingKeyRegistrationEnabled] = strconv.FormatBool(settings.RegistrationEnabled) + updates[model.SettingKeyEmailVerifyEnabled] = strconv.FormatBool(settings.EmailVerifyEnabled) + + // 邮件服务设置(只有非空才更新密码) + updates[model.SettingKeySmtpHost] = settings.SmtpHost + updates[model.SettingKeySmtpPort] = strconv.Itoa(settings.SmtpPort) + updates[model.SettingKeySmtpUsername] = settings.SmtpUsername + if settings.SmtpPassword != "" { + updates[model.SettingKeySmtpPassword] = settings.SmtpPassword + } + updates[model.SettingKeySmtpFrom] = settings.SmtpFrom + updates[model.SettingKeySmtpFromName] = settings.SmtpFromName + updates[model.SettingKeySmtpUseTLS] = strconv.FormatBool(settings.SmtpUseTLS) + + // Cloudflare Turnstile 设置(只有非空才更新密钥) + updates[model.SettingKeyTurnstileEnabled] = strconv.FormatBool(settings.TurnstileEnabled) + updates[model.SettingKeyTurnstileSiteKey] = settings.TurnstileSiteKey + if settings.TurnstileSecretKey != "" { + updates[model.SettingKeyTurnstileSecretKey] = settings.TurnstileSecretKey + } + + // OEM设置 + updates[model.SettingKeySiteName] = settings.SiteName + updates[model.SettingKeySiteLogo] = settings.SiteLogo + updates[model.SettingKeySiteSubtitle] = settings.SiteSubtitle + updates[model.SettingKeyApiBaseUrl] = settings.ApiBaseUrl + updates[model.SettingKeyContactInfo] = settings.ContactInfo + + // 默认配置 + updates[model.SettingKeyDefaultConcurrency] = strconv.Itoa(settings.DefaultConcurrency) + updates[model.SettingKeyDefaultBalance] = strconv.FormatFloat(settings.DefaultBalance, 'f', 8, 64) + + return s.settingRepo.SetMultiple(ctx, updates) +} + +// IsRegistrationEnabled 检查是否开放注册 +func (s *SettingService) IsRegistrationEnabled(ctx context.Context) bool { + value, err := s.settingRepo.GetValue(ctx, model.SettingKeyRegistrationEnabled) + if err != nil { + // 默认开放注册 + return true + } + return value == "true" +} + +// IsEmailVerifyEnabled 检查是否开启邮件验证 +func (s *SettingService) IsEmailVerifyEnabled(ctx context.Context) bool { + value, err := s.settingRepo.GetValue(ctx, model.SettingKeyEmailVerifyEnabled) + if err != nil { + return false + } + return value == "true" +} + +// GetSiteName 获取网站名称 +func (s *SettingService) GetSiteName(ctx context.Context) string { + value, err := s.settingRepo.GetValue(ctx, model.SettingKeySiteName) + if err != nil || value == "" { + return "Sub2API" + } + return value +} + +// GetDefaultConcurrency 获取默认并发量 +func (s *SettingService) GetDefaultConcurrency(ctx context.Context) int { + value, err := s.settingRepo.GetValue(ctx, model.SettingKeyDefaultConcurrency) + if err != nil { + return s.cfg.Default.UserConcurrency + } + if v, err := strconv.Atoi(value); err == nil && v > 0 { + return v + } + return s.cfg.Default.UserConcurrency +} + +// GetDefaultBalance 获取默认余额 +func (s *SettingService) GetDefaultBalance(ctx context.Context) float64 { + value, err := s.settingRepo.GetValue(ctx, model.SettingKeyDefaultBalance) + if err != nil { + return s.cfg.Default.UserBalance + } + if v, err := strconv.ParseFloat(value, 64); err == nil && v >= 0 { + return v + } + return s.cfg.Default.UserBalance +} + +// InitializeDefaultSettings 初始化默认设置 +func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error { + // 检查是否已有设置 + _, err := s.settingRepo.GetValue(ctx, model.SettingKeyRegistrationEnabled) + if err == nil { + // 已有设置,不需要初始化 + return nil + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("check existing settings: %w", err) + } + + // 初始化默认设置 + defaults := map[string]string{ + model.SettingKeyRegistrationEnabled: "true", + model.SettingKeyEmailVerifyEnabled: "false", + model.SettingKeySiteName: "Sub2API", + model.SettingKeySiteLogo: "", + model.SettingKeyDefaultConcurrency: strconv.Itoa(s.cfg.Default.UserConcurrency), + model.SettingKeyDefaultBalance: strconv.FormatFloat(s.cfg.Default.UserBalance, 'f', 8, 64), + model.SettingKeySmtpPort: "587", + model.SettingKeySmtpUseTLS: "false", + } + + return s.settingRepo.SetMultiple(ctx, defaults) +} + +// parseSettings 解析设置到结构体 +func (s *SettingService) parseSettings(settings map[string]string) *model.SystemSettings { + result := &model.SystemSettings{ + RegistrationEnabled: settings[model.SettingKeyRegistrationEnabled] == "true", + EmailVerifyEnabled: settings[model.SettingKeyEmailVerifyEnabled] == "true", + SmtpHost: settings[model.SettingKeySmtpHost], + SmtpUsername: settings[model.SettingKeySmtpUsername], + SmtpFrom: settings[model.SettingKeySmtpFrom], + SmtpFromName: settings[model.SettingKeySmtpFromName], + SmtpUseTLS: settings[model.SettingKeySmtpUseTLS] == "true", + TurnstileEnabled: settings[model.SettingKeyTurnstileEnabled] == "true", + TurnstileSiteKey: settings[model.SettingKeyTurnstileSiteKey], + SiteName: s.getStringOrDefault(settings, model.SettingKeySiteName, "Sub2API"), + SiteLogo: settings[model.SettingKeySiteLogo], + SiteSubtitle: s.getStringOrDefault(settings, model.SettingKeySiteSubtitle, "Subscription to API Conversion Platform"), + ApiBaseUrl: settings[model.SettingKeyApiBaseUrl], + ContactInfo: settings[model.SettingKeyContactInfo], + } + + // 解析整数类型 + if port, err := strconv.Atoi(settings[model.SettingKeySmtpPort]); err == nil { + result.SmtpPort = port + } else { + result.SmtpPort = 587 + } + + if concurrency, err := strconv.Atoi(settings[model.SettingKeyDefaultConcurrency]); err == nil { + result.DefaultConcurrency = concurrency + } else { + result.DefaultConcurrency = s.cfg.Default.UserConcurrency + } + + // 解析浮点数类型 + if balance, err := strconv.ParseFloat(settings[model.SettingKeyDefaultBalance], 64); err == nil { + result.DefaultBalance = balance + } else { + result.DefaultBalance = s.cfg.Default.UserBalance + } + + // 敏感信息直接返回,方便测试连接时使用 + result.SmtpPassword = settings[model.SettingKeySmtpPassword] + result.TurnstileSecretKey = settings[model.SettingKeyTurnstileSecretKey] + + return result +} + +// getStringOrDefault 获取字符串值或默认值 +func (s *SettingService) getStringOrDefault(settings map[string]string, key, defaultValue string) string { + if value, ok := settings[key]; ok && value != "" { + return value + } + return defaultValue +} + +// IsTurnstileEnabled 检查是否启用 Turnstile 验证 +func (s *SettingService) IsTurnstileEnabled(ctx context.Context) bool { + value, err := s.settingRepo.GetValue(ctx, model.SettingKeyTurnstileEnabled) + if err != nil { + return false + } + return value == "true" +} + +// GetTurnstileSecretKey 获取 Turnstile Secret Key +func (s *SettingService) GetTurnstileSecretKey(ctx context.Context) string { + value, err := s.settingRepo.GetValue(ctx, model.SettingKeyTurnstileSecretKey) + if err != nil { + return "" + } + return value +} diff --git a/backend/internal/service/subscription_service.go b/backend/internal/service/subscription_service.go new file mode 100644 index 00000000..16b17410 --- /dev/null +++ b/backend/internal/service/subscription_service.go @@ -0,0 +1,575 @@ +package service + +import ( + "context" + "errors" + "fmt" + "time" + + "sub2api/internal/model" + "sub2api/internal/repository" +) + +var ( + ErrSubscriptionNotFound = errors.New("subscription not found") + ErrSubscriptionExpired = errors.New("subscription has expired") + ErrSubscriptionSuspended = errors.New("subscription is suspended") + ErrSubscriptionAlreadyExists = errors.New("subscription already exists for this user and group") + ErrGroupNotSubscriptionType = errors.New("group is not a subscription type") + ErrDailyLimitExceeded = errors.New("daily usage limit exceeded") + ErrWeeklyLimitExceeded = errors.New("weekly usage limit exceeded") + ErrMonthlyLimitExceeded = errors.New("monthly usage limit exceeded") +) + +// SubscriptionService 订阅服务 +type SubscriptionService struct { + repos *repository.Repositories + billingCacheService *BillingCacheService +} + +// NewSubscriptionService 创建订阅服务 +func NewSubscriptionService(repos *repository.Repositories) *SubscriptionService { + return &SubscriptionService{repos: repos} +} + +// SetBillingCacheService 设置计费缓存服务(用于缓存失效) +func (s *SubscriptionService) SetBillingCacheService(billingCacheService *BillingCacheService) { + s.billingCacheService = billingCacheService +} + +// AssignSubscriptionInput 分配订阅输入 +type AssignSubscriptionInput struct { + UserID int64 + GroupID int64 + ValidityDays int + AssignedBy int64 + Notes string +} + +// AssignSubscription 分配订阅给用户(不允许重复分配) +func (s *SubscriptionService) AssignSubscription(ctx context.Context, input *AssignSubscriptionInput) (*model.UserSubscription, error) { + // 检查分组是否存在且为订阅类型 + group, err := s.repos.Group.GetByID(ctx, input.GroupID) + if err != nil { + return nil, fmt.Errorf("group not found: %w", err) + } + if !group.IsSubscriptionType() { + return nil, ErrGroupNotSubscriptionType + } + + // 检查是否已存在订阅 + exists, err := s.repos.UserSubscription.ExistsByUserIDAndGroupID(ctx, input.UserID, input.GroupID) + if err != nil { + return nil, err + } + if exists { + return nil, ErrSubscriptionAlreadyExists + } + + sub, err := s.createSubscription(ctx, input) + if err != nil { + return nil, err + } + + // 失效订阅缓存 + if s.billingCacheService != nil { + userID, groupID := input.UserID, input.GroupID + go func() { + cacheCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + s.billingCacheService.InvalidateSubscription(cacheCtx, userID, groupID) + }() + } + + return sub, nil +} + +// AssignOrExtendSubscription 分配或续期订阅(用于兑换码等场景) +// 如果用户已有同分组的订阅: +// - 未过期:从当前过期时间累加天数 +// - 已过期:从当前时间开始计算新的过期时间,并激活订阅 +// 如果没有订阅:创建新订阅 +func (s *SubscriptionService) AssignOrExtendSubscription(ctx context.Context, input *AssignSubscriptionInput) (*model.UserSubscription, bool, error) { + // 检查分组是否存在且为订阅类型 + group, err := s.repos.Group.GetByID(ctx, input.GroupID) + if err != nil { + return nil, false, fmt.Errorf("group not found: %w", err) + } + if !group.IsSubscriptionType() { + return nil, false, ErrGroupNotSubscriptionType + } + + // 查询是否已有订阅 + existingSub, err := s.repos.UserSubscription.GetByUserIDAndGroupID(ctx, input.UserID, input.GroupID) + if err != nil { + // 不存在记录是正常情况,其他错误需要返回 + existingSub = nil + } + + validityDays := input.ValidityDays + if validityDays <= 0 { + validityDays = 30 + } + + // 已有订阅,执行续期 + if existingSub != nil { + now := time.Now() + var newExpiresAt time.Time + + if existingSub.ExpiresAt.After(now) { + // 未过期:从当前过期时间累加 + newExpiresAt = existingSub.ExpiresAt.AddDate(0, 0, validityDays) + } else { + // 已过期:从当前时间开始计算 + newExpiresAt = now.AddDate(0, 0, validityDays) + } + + // 更新过期时间 + if err := s.repos.UserSubscription.ExtendExpiry(ctx, existingSub.ID, newExpiresAt); err != nil { + return nil, false, fmt.Errorf("extend subscription: %w", err) + } + + // 如果订阅已过期或被暂停,恢复为active状态 + if existingSub.Status != model.SubscriptionStatusActive { + if err := s.repos.UserSubscription.UpdateStatus(ctx, existingSub.ID, model.SubscriptionStatusActive); err != nil { + return nil, false, fmt.Errorf("update subscription status: %w", err) + } + } + + // 追加备注 + if input.Notes != "" { + newNotes := existingSub.Notes + if newNotes != "" { + newNotes += "\n" + } + newNotes += input.Notes + if err := s.repos.UserSubscription.UpdateNotes(ctx, existingSub.ID, newNotes); err != nil { + // 备注更新失败不影响主流程 + } + } + + // 失效订阅缓存 + if s.billingCacheService != nil { + userID, groupID := input.UserID, input.GroupID + go func() { + cacheCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + s.billingCacheService.InvalidateSubscription(cacheCtx, userID, groupID) + }() + } + + // 返回更新后的订阅 + sub, err := s.repos.UserSubscription.GetByID(ctx, existingSub.ID) + return sub, true, err // true 表示是续期 + } + + // 没有订阅,创建新订阅 + sub, err := s.createSubscription(ctx, input) + if err != nil { + return nil, false, err + } + + // 失效订阅缓存 + if s.billingCacheService != nil { + userID, groupID := input.UserID, input.GroupID + go func() { + cacheCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + s.billingCacheService.InvalidateSubscription(cacheCtx, userID, groupID) + }() + } + + return sub, false, nil // false 表示是新建 +} + +// createSubscription 创建新订阅(内部方法) +func (s *SubscriptionService) createSubscription(ctx context.Context, input *AssignSubscriptionInput) (*model.UserSubscription, error) { + validityDays := input.ValidityDays + if validityDays <= 0 { + validityDays = 30 + } + + now := time.Now() + sub := &model.UserSubscription{ + UserID: input.UserID, + GroupID: input.GroupID, + StartsAt: now, + ExpiresAt: now.AddDate(0, 0, validityDays), + Status: model.SubscriptionStatusActive, + AssignedAt: now, + Notes: input.Notes, + CreatedAt: now, + UpdatedAt: now, + } + // 只有当 AssignedBy > 0 时才设置(0 表示系统分配,如兑换码) + if input.AssignedBy > 0 { + sub.AssignedBy = &input.AssignedBy + } + + if err := s.repos.UserSubscription.Create(ctx, sub); err != nil { + return nil, err + } + + // 重新获取完整订阅信息(包含关联) + return s.repos.UserSubscription.GetByID(ctx, sub.ID) +} + +// BulkAssignSubscriptionInput 批量分配订阅输入 +type BulkAssignSubscriptionInput struct { + UserIDs []int64 + GroupID int64 + ValidityDays int + AssignedBy int64 + Notes string +} + +// BulkAssignResult 批量分配结果 +type BulkAssignResult struct { + SuccessCount int + FailedCount int + Subscriptions []model.UserSubscription + Errors []string +} + +// BulkAssignSubscription 批量分配订阅 +func (s *SubscriptionService) BulkAssignSubscription(ctx context.Context, input *BulkAssignSubscriptionInput) (*BulkAssignResult, error) { + result := &BulkAssignResult{ + Subscriptions: make([]model.UserSubscription, 0), + Errors: make([]string, 0), + } + + for _, userID := range input.UserIDs { + sub, err := s.AssignSubscription(ctx, &AssignSubscriptionInput{ + UserID: userID, + GroupID: input.GroupID, + ValidityDays: input.ValidityDays, + AssignedBy: input.AssignedBy, + Notes: input.Notes, + }) + if err != nil { + result.FailedCount++ + result.Errors = append(result.Errors, fmt.Sprintf("user %d: %v", userID, err)) + } else { + result.SuccessCount++ + result.Subscriptions = append(result.Subscriptions, *sub) + } + } + + return result, nil +} + +// RevokeSubscription 撤销订阅 +func (s *SubscriptionService) RevokeSubscription(ctx context.Context, subscriptionID int64) error { + // 先获取订阅信息用于失效缓存 + sub, err := s.repos.UserSubscription.GetByID(ctx, subscriptionID) + if err != nil { + return err + } + + if err := s.repos.UserSubscription.Delete(ctx, subscriptionID); err != nil { + return err + } + + // 失效订阅缓存 + if s.billingCacheService != nil { + userID, groupID := sub.UserID, sub.GroupID + go func() { + cacheCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + s.billingCacheService.InvalidateSubscription(cacheCtx, userID, groupID) + }() + } + + return nil +} + +// ExtendSubscription 延长订阅 +func (s *SubscriptionService) ExtendSubscription(ctx context.Context, subscriptionID int64, days int) (*model.UserSubscription, error) { + sub, err := s.repos.UserSubscription.GetByID(ctx, subscriptionID) + if err != nil { + return nil, ErrSubscriptionNotFound + } + + // 计算新的过期时间 + newExpiresAt := sub.ExpiresAt.AddDate(0, 0, days) + if err := s.repos.UserSubscription.ExtendExpiry(ctx, subscriptionID, newExpiresAt); err != nil { + return nil, err + } + + // 如果订阅已过期,恢复为active状态 + if sub.Status == model.SubscriptionStatusExpired { + if err := s.repos.UserSubscription.UpdateStatus(ctx, subscriptionID, model.SubscriptionStatusActive); err != nil { + return nil, err + } + } + + // 失效订阅缓存 + if s.billingCacheService != nil { + userID, groupID := sub.UserID, sub.GroupID + go func() { + cacheCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + s.billingCacheService.InvalidateSubscription(cacheCtx, userID, groupID) + }() + } + + return s.repos.UserSubscription.GetByID(ctx, subscriptionID) +} + +// GetByID 根据ID获取订阅 +func (s *SubscriptionService) GetByID(ctx context.Context, id int64) (*model.UserSubscription, error) { + return s.repos.UserSubscription.GetByID(ctx, id) +} + +// GetActiveSubscription 获取用户对特定分组的有效订阅 +func (s *SubscriptionService) GetActiveSubscription(ctx context.Context, userID, groupID int64) (*model.UserSubscription, error) { + sub, err := s.repos.UserSubscription.GetActiveByUserIDAndGroupID(ctx, userID, groupID) + if err != nil { + return nil, ErrSubscriptionNotFound + } + return sub, nil +} + +// ListUserSubscriptions 获取用户的所有订阅 +func (s *SubscriptionService) ListUserSubscriptions(ctx context.Context, userID int64) ([]model.UserSubscription, error) { + return s.repos.UserSubscription.ListByUserID(ctx, userID) +} + +// ListActiveUserSubscriptions 获取用户的所有有效订阅 +func (s *SubscriptionService) ListActiveUserSubscriptions(ctx context.Context, userID int64) ([]model.UserSubscription, error) { + return s.repos.UserSubscription.ListActiveByUserID(ctx, userID) +} + +// ListGroupSubscriptions 获取分组的所有订阅 +func (s *SubscriptionService) ListGroupSubscriptions(ctx context.Context, groupID int64, page, pageSize int) ([]model.UserSubscription, *repository.PaginationResult, error) { + params := repository.PaginationParams{Page: page, PageSize: pageSize} + return s.repos.UserSubscription.ListByGroupID(ctx, groupID, params) +} + +// List 获取所有订阅(分页,支持筛选) +func (s *SubscriptionService) List(ctx context.Context, page, pageSize int, userID, groupID *int64, status string) ([]model.UserSubscription, *repository.PaginationResult, error) { + params := repository.PaginationParams{Page: page, PageSize: pageSize} + return s.repos.UserSubscription.List(ctx, params, userID, groupID, status) +} + +// CheckAndActivateWindow 检查并激活窗口(首次使用时) +func (s *SubscriptionService) CheckAndActivateWindow(ctx context.Context, sub *model.UserSubscription) error { + if sub.IsWindowActivated() { + return nil + } + + now := time.Now() + return s.repos.UserSubscription.ActivateWindows(ctx, sub.ID, now) +} + +// CheckAndResetWindows 检查并重置过期的窗口 +func (s *SubscriptionService) CheckAndResetWindows(ctx context.Context, sub *model.UserSubscription) error { + now := time.Now() + + // 日窗口重置(24小时) + if sub.NeedsDailyReset() { + if err := s.repos.UserSubscription.ResetDailyUsage(ctx, sub.ID, now); err != nil { + return err + } + sub.DailyWindowStart = &now + sub.DailyUsageUSD = 0 + } + + // 周窗口重置(7天) + if sub.NeedsWeeklyReset() { + if err := s.repos.UserSubscription.ResetWeeklyUsage(ctx, sub.ID, now); err != nil { + return err + } + sub.WeeklyWindowStart = &now + sub.WeeklyUsageUSD = 0 + } + + // 月窗口重置(30天) + if sub.NeedsMonthlyReset() { + if err := s.repos.UserSubscription.ResetMonthlyUsage(ctx, sub.ID, now); err != nil { + return err + } + sub.MonthlyWindowStart = &now + sub.MonthlyUsageUSD = 0 + } + + return nil +} + +// CheckUsageLimits 检查使用限额(返回错误如果超限) +func (s *SubscriptionService) CheckUsageLimits(ctx context.Context, sub *model.UserSubscription, group *model.Group, additionalCost float64) error { + if !sub.CheckDailyLimit(group, additionalCost) { + return ErrDailyLimitExceeded + } + if !sub.CheckWeeklyLimit(group, additionalCost) { + return ErrWeeklyLimitExceeded + } + if !sub.CheckMonthlyLimit(group, additionalCost) { + return ErrMonthlyLimitExceeded + } + return nil +} + +// RecordUsage 记录使用量到订阅 +func (s *SubscriptionService) RecordUsage(ctx context.Context, subscriptionID int64, costUSD float64) error { + return s.repos.UserSubscription.IncrementUsage(ctx, subscriptionID, costUSD) +} + +// SubscriptionProgress 订阅进度 +type SubscriptionProgress struct { + ID int64 `json:"id"` + GroupName string `json:"group_name"` + ExpiresAt time.Time `json:"expires_at"` + ExpiresInDays int `json:"expires_in_days"` + Daily *UsageWindowProgress `json:"daily,omitempty"` + Weekly *UsageWindowProgress `json:"weekly,omitempty"` + Monthly *UsageWindowProgress `json:"monthly,omitempty"` +} + +// UsageWindowProgress 使用窗口进度 +type UsageWindowProgress struct { + LimitUSD float64 `json:"limit_usd"` + UsedUSD float64 `json:"used_usd"` + RemainingUSD float64 `json:"remaining_usd"` + Percentage float64 `json:"percentage"` + WindowStart time.Time `json:"window_start"` + ResetsAt time.Time `json:"resets_at"` + ResetsInSeconds int64 `json:"resets_in_seconds"` +} + +// GetSubscriptionProgress 获取订阅使用进度 +func (s *SubscriptionService) GetSubscriptionProgress(ctx context.Context, subscriptionID int64) (*SubscriptionProgress, error) { + sub, err := s.repos.UserSubscription.GetByID(ctx, subscriptionID) + if err != nil { + return nil, ErrSubscriptionNotFound + } + + group := sub.Group + if group == nil { + group, err = s.repos.Group.GetByID(ctx, sub.GroupID) + if err != nil { + return nil, err + } + } + + progress := &SubscriptionProgress{ + ID: sub.ID, + GroupName: group.Name, + ExpiresAt: sub.ExpiresAt, + ExpiresInDays: sub.DaysRemaining(), + } + + // 日进度 + if group.HasDailyLimit() && sub.DailyWindowStart != nil { + limit := *group.DailyLimitUSD + resetsAt := sub.DailyWindowStart.Add(24 * time.Hour) + progress.Daily = &UsageWindowProgress{ + LimitUSD: limit, + UsedUSD: sub.DailyUsageUSD, + RemainingUSD: limit - sub.DailyUsageUSD, + Percentage: (sub.DailyUsageUSD / limit) * 100, + WindowStart: *sub.DailyWindowStart, + ResetsAt: resetsAt, + ResetsInSeconds: int64(time.Until(resetsAt).Seconds()), + } + if progress.Daily.RemainingUSD < 0 { + progress.Daily.RemainingUSD = 0 + } + if progress.Daily.Percentage > 100 { + progress.Daily.Percentage = 100 + } + if progress.Daily.ResetsInSeconds < 0 { + progress.Daily.ResetsInSeconds = 0 + } + } + + // 周进度 + if group.HasWeeklyLimit() && sub.WeeklyWindowStart != nil { + limit := *group.WeeklyLimitUSD + resetsAt := sub.WeeklyWindowStart.Add(7 * 24 * time.Hour) + progress.Weekly = &UsageWindowProgress{ + LimitUSD: limit, + UsedUSD: sub.WeeklyUsageUSD, + RemainingUSD: limit - sub.WeeklyUsageUSD, + Percentage: (sub.WeeklyUsageUSD / limit) * 100, + WindowStart: *sub.WeeklyWindowStart, + ResetsAt: resetsAt, + ResetsInSeconds: int64(time.Until(resetsAt).Seconds()), + } + if progress.Weekly.RemainingUSD < 0 { + progress.Weekly.RemainingUSD = 0 + } + if progress.Weekly.Percentage > 100 { + progress.Weekly.Percentage = 100 + } + if progress.Weekly.ResetsInSeconds < 0 { + progress.Weekly.ResetsInSeconds = 0 + } + } + + // 月进度 + if group.HasMonthlyLimit() && sub.MonthlyWindowStart != nil { + limit := *group.MonthlyLimitUSD + resetsAt := sub.MonthlyWindowStart.Add(30 * 24 * time.Hour) + progress.Monthly = &UsageWindowProgress{ + LimitUSD: limit, + UsedUSD: sub.MonthlyUsageUSD, + RemainingUSD: limit - sub.MonthlyUsageUSD, + Percentage: (sub.MonthlyUsageUSD / limit) * 100, + WindowStart: *sub.MonthlyWindowStart, + ResetsAt: resetsAt, + ResetsInSeconds: int64(time.Until(resetsAt).Seconds()), + } + if progress.Monthly.RemainingUSD < 0 { + progress.Monthly.RemainingUSD = 0 + } + if progress.Monthly.Percentage > 100 { + progress.Monthly.Percentage = 100 + } + if progress.Monthly.ResetsInSeconds < 0 { + progress.Monthly.ResetsInSeconds = 0 + } + } + + return progress, nil +} + +// GetUserSubscriptionsWithProgress 获取用户所有订阅及进度 +func (s *SubscriptionService) GetUserSubscriptionsWithProgress(ctx context.Context, userID int64) ([]SubscriptionProgress, error) { + subs, err := s.repos.UserSubscription.ListActiveByUserID(ctx, userID) + if err != nil { + return nil, err + } + + progresses := make([]SubscriptionProgress, 0, len(subs)) + for _, sub := range subs { + progress, err := s.GetSubscriptionProgress(ctx, sub.ID) + if err != nil { + continue + } + progresses = append(progresses, *progress) + } + + return progresses, nil +} + +// UpdateExpiredSubscriptions 更新过期订阅状态(定时任务调用) +func (s *SubscriptionService) UpdateExpiredSubscriptions(ctx context.Context) (int64, error) { + return s.repos.UserSubscription.BatchUpdateExpiredStatus(ctx) +} + +// ValidateSubscription 验证订阅是否有效 +func (s *SubscriptionService) ValidateSubscription(ctx context.Context, sub *model.UserSubscription) error { + if sub.Status == model.SubscriptionStatusExpired { + return ErrSubscriptionExpired + } + if sub.Status == model.SubscriptionStatusSuspended { + return ErrSubscriptionSuspended + } + if sub.IsExpired() { + // 更新状态 + _ = s.repos.UserSubscription.UpdateStatus(ctx, sub.ID, model.SubscriptionStatusExpired) + return ErrSubscriptionExpired + } + return nil +} diff --git a/backend/internal/service/turnstile_service.go b/backend/internal/service/turnstile_service.go new file mode 100644 index 00000000..7603c782 --- /dev/null +++ b/backend/internal/service/turnstile_service.go @@ -0,0 +1,111 @@ +package service + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "net/url" + "strings" + "time" +) + +var ( + ErrTurnstileVerificationFailed = errors.New("turnstile verification failed") + ErrTurnstileNotConfigured = errors.New("turnstile not configured") +) + +const turnstileVerifyURL = "https://challenges.cloudflare.com/turnstile/v0/siteverify" + +// TurnstileService Turnstile 验证服务 +type TurnstileService struct { + settingService *SettingService + httpClient *http.Client +} + +// TurnstileVerifyResponse Cloudflare Turnstile 验证响应 +type TurnstileVerifyResponse struct { + Success bool `json:"success"` + ChallengeTS string `json:"challenge_ts"` + Hostname string `json:"hostname"` + ErrorCodes []string `json:"error-codes"` + Action string `json:"action"` + CData string `json:"cdata"` +} + +// NewTurnstileService 创建 Turnstile 服务实例 +func NewTurnstileService(settingService *SettingService) *TurnstileService { + return &TurnstileService{ + settingService: settingService, + httpClient: &http.Client{ + Timeout: 10 * time.Second, + }, + } +} + +// VerifyToken 验证 Turnstile token +func (s *TurnstileService) VerifyToken(ctx context.Context, token string, remoteIP string) error { + // 检查是否启用 Turnstile + if !s.settingService.IsTurnstileEnabled(ctx) { + log.Println("[Turnstile] Disabled, skipping verification") + return nil + } + + // 获取 Secret Key + secretKey := s.settingService.GetTurnstileSecretKey(ctx) + if secretKey == "" { + log.Println("[Turnstile] Secret key not configured") + return ErrTurnstileNotConfigured + } + + // 如果 token 为空,返回错误 + if token == "" { + log.Println("[Turnstile] Token is empty") + return ErrTurnstileVerificationFailed + } + + // 构建请求 + formData := url.Values{} + formData.Set("secret", secretKey) + formData.Set("response", token) + if remoteIP != "" { + formData.Set("remoteip", remoteIP) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, turnstileVerifyURL, strings.NewReader(formData.Encode())) + if err != nil { + return fmt.Errorf("create request: %w", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + // 发送请求 + log.Printf("[Turnstile] Verifying token for IP: %s", remoteIP) + resp, err := s.httpClient.Do(req) + if err != nil { + log.Printf("[Turnstile] Request failed: %v", err) + return fmt.Errorf("send request: %w", err) + } + defer resp.Body.Close() + + // 解析响应 + var result TurnstileVerifyResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + log.Printf("[Turnstile] Failed to decode response: %v", err) + return fmt.Errorf("decode response: %w", err) + } + + if !result.Success { + log.Printf("[Turnstile] Verification failed, error codes: %v", result.ErrorCodes) + return ErrTurnstileVerificationFailed + } + + log.Println("[Turnstile] Verification successful") + return nil +} + +// IsEnabled 检查 Turnstile 是否启用 +func (s *TurnstileService) IsEnabled(ctx context.Context) bool { + return s.settingService.IsTurnstileEnabled(ctx) +} diff --git a/backend/internal/service/update_service.go b/backend/internal/service/update_service.go new file mode 100644 index 00000000..54c0fd16 --- /dev/null +++ b/backend/internal/service/update_service.go @@ -0,0 +1,621 @@ +package service + +import ( + "archive/tar" + "bufio" + "compress/gzip" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/redis/go-redis/v9" +) + +const ( + updateCacheKey = "update_check_cache" + updateCacheTTL = 1200 // 20 minutes + githubRepo = "Wei-Shaw/sub2api" + + // Security: allowed download domains for updates + allowedDownloadHost = "github.com" + allowedAssetHost = "objects.githubusercontent.com" + + // Security: max download size (500MB) + maxDownloadSize = 500 * 1024 * 1024 +) + +// UpdateService handles software updates +type UpdateService struct { + rdb *redis.Client + currentVersion string + buildType string // "source" for manual builds, "release" for CI builds +} + +// NewUpdateService creates a new UpdateService +func NewUpdateService(rdb *redis.Client, version, buildType string) *UpdateService { + return &UpdateService{ + rdb: rdb, + currentVersion: version, + buildType: buildType, + } +} + +// UpdateInfo contains update information +type UpdateInfo struct { + CurrentVersion string `json:"current_version"` + LatestVersion string `json:"latest_version"` + HasUpdate bool `json:"has_update"` + ReleaseInfo *ReleaseInfo `json:"release_info,omitempty"` + Cached bool `json:"cached"` + Warning string `json:"warning,omitempty"` + BuildType string `json:"build_type"` // "source" or "release" +} + +// ReleaseInfo contains GitHub release details +type ReleaseInfo struct { + Name string `json:"name"` + Body string `json:"body"` + PublishedAt string `json:"published_at"` + HtmlURL string `json:"html_url"` + Assets []Asset `json:"assets,omitempty"` +} + +// Asset represents a release asset +type Asset struct { + Name string `json:"name"` + DownloadURL string `json:"download_url"` + Size int64 `json:"size"` +} + +// GitHubRelease represents GitHub API response +type GitHubRelease struct { + TagName string `json:"tag_name"` + Name string `json:"name"` + Body string `json:"body"` + PublishedAt string `json:"published_at"` + HtmlUrl string `json:"html_url"` + Assets []GitHubAsset `json:"assets"` +} + +type GitHubAsset struct { + Name string `json:"name"` + BrowserDownloadUrl string `json:"browser_download_url"` + Size int64 `json:"size"` +} + +// CheckUpdate checks for available updates +func (s *UpdateService) CheckUpdate(ctx context.Context, force bool) (*UpdateInfo, error) { + // Try cache first + if !force { + if cached, err := s.getFromCache(ctx); err == nil && cached != nil { + return cached, nil + } + } + + // Fetch from GitHub + info, err := s.fetchLatestRelease(ctx) + if err != nil { + // Return cached on error + if cached, cacheErr := s.getFromCache(ctx); cacheErr == nil && cached != nil { + cached.Warning = "Using cached data: " + err.Error() + return cached, nil + } + return &UpdateInfo{ + CurrentVersion: s.currentVersion, + LatestVersion: s.currentVersion, + HasUpdate: false, + Warning: err.Error(), + BuildType: s.buildType, + }, nil + } + + // Cache result + s.saveToCache(ctx, info) + return info, nil +} + +// PerformUpdate downloads and applies the update +func (s *UpdateService) PerformUpdate(ctx context.Context) error { + info, err := s.CheckUpdate(ctx, true) + if err != nil { + return err + } + + if !info.HasUpdate { + return fmt.Errorf("no update available") + } + + // Find matching archive and checksum for current platform + archiveName := s.getArchiveName() + var downloadURL string + var checksumURL string + + for _, asset := range info.ReleaseInfo.Assets { + if strings.Contains(asset.Name, archiveName) && !strings.HasSuffix(asset.Name, ".txt") { + downloadURL = asset.DownloadURL + } + if asset.Name == "checksums.txt" { + checksumURL = asset.DownloadURL + } + } + + if downloadURL == "" { + return fmt.Errorf("no compatible release found for %s/%s", runtime.GOOS, runtime.GOARCH) + } + + // SECURITY: Validate download URL is from trusted domain + if err := validateDownloadURL(downloadURL); err != nil { + return fmt.Errorf("invalid download URL: %w", err) + } + if checksumURL != "" { + if err := validateDownloadURL(checksumURL); err != nil { + return fmt.Errorf("invalid checksum URL: %w", err) + } + } + + // Get current executable path + exePath, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to get executable path: %w", err) + } + exePath, err = filepath.EvalSymlinks(exePath) + if err != nil { + return fmt.Errorf("failed to resolve symlinks: %w", err) + } + + // Create temp directory for extraction + tempDir, err := os.MkdirTemp("", "sub2api-update-*") + if err != nil { + return fmt.Errorf("failed to create temp dir: %w", err) + } + defer os.RemoveAll(tempDir) + + // Download archive + archivePath := filepath.Join(tempDir, filepath.Base(downloadURL)) + if err := s.downloadFile(ctx, downloadURL, archivePath); err != nil { + return fmt.Errorf("download failed: %w", err) + } + + // Verify checksum if available + if checksumURL != "" { + if err := s.verifyChecksum(ctx, archivePath, checksumURL); err != nil { + return fmt.Errorf("checksum verification failed: %w", err) + } + } + + // Extract binary from archive + newBinaryPath := filepath.Join(tempDir, "sub2api") + if err := s.extractBinary(archivePath, newBinaryPath); err != nil { + return fmt.Errorf("extraction failed: %w", err) + } + + // Backup current binary + backupFile := exePath + ".backup" + if err := os.Rename(exePath, backupFile); err != nil { + return fmt.Errorf("backup failed: %w", err) + } + + // Replace with new binary + if err := copyFile(newBinaryPath, exePath); err != nil { + os.Rename(backupFile, exePath) + return fmt.Errorf("replace failed: %w", err) + } + + // Make executable + if err := os.Chmod(exePath, 0755); err != nil { + return fmt.Errorf("chmod failed: %w", err) + } + + return nil +} + +// Rollback restores the previous version +func (s *UpdateService) Rollback() error { + exePath, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to get executable path: %w", err) + } + exePath, err = filepath.EvalSymlinks(exePath) + if err != nil { + return fmt.Errorf("failed to resolve symlinks: %w", err) + } + + backupFile := exePath + ".backup" + if _, err := os.Stat(backupFile); os.IsNotExist(err) { + return fmt.Errorf("no backup found") + } + + // Replace current with backup + if err := os.Rename(backupFile, exePath); err != nil { + return fmt.Errorf("rollback failed: %w", err) + } + + return nil +} + +// RestartService triggers a service restart via systemd +func (s *UpdateService) RestartService() error { + if runtime.GOOS != "linux" { + return fmt.Errorf("systemd restart only available on Linux") + } + + // Try direct systemctl first (works if running as root or with proper permissions) + cmd := exec.Command("systemctl", "restart", "sub2api") + if err := cmd.Run(); err != nil { + // Try with sudo (requires NOPASSWD sudoers entry) + sudoCmd := exec.Command("sudo", "systemctl", "restart", "sub2api") + if sudoErr := sudoCmd.Run(); sudoErr != nil { + return fmt.Errorf("systemctl restart failed: %w (sudo also failed: %v)", err, sudoErr) + } + } + return nil +} + +func (s *UpdateService) fetchLatestRelease(ctx context.Context) (*UpdateInfo, error) { + url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", githubRepo) + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", "application/vnd.github.v3+json") + req.Header.Set("User-Agent", "Sub2API-Updater") + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + return &UpdateInfo{ + CurrentVersion: s.currentVersion, + LatestVersion: s.currentVersion, + HasUpdate: false, + Warning: "No releases found", + BuildType: s.buildType, + }, nil + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("GitHub API returned %d", resp.StatusCode) + } + + var release GitHubRelease + if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { + return nil, err + } + + latestVersion := strings.TrimPrefix(release.TagName, "v") + + assets := make([]Asset, len(release.Assets)) + for i, a := range release.Assets { + assets[i] = Asset{ + Name: a.Name, + DownloadURL: a.BrowserDownloadUrl, + Size: a.Size, + } + } + + return &UpdateInfo{ + CurrentVersion: s.currentVersion, + LatestVersion: latestVersion, + HasUpdate: compareVersions(s.currentVersion, latestVersion) < 0, + ReleaseInfo: &ReleaseInfo{ + Name: release.Name, + Body: release.Body, + PublishedAt: release.PublishedAt, + HtmlURL: release.HtmlUrl, + Assets: assets, + }, + Cached: false, + BuildType: s.buildType, + }, nil +} + +func (s *UpdateService) downloadFile(ctx context.Context, downloadURL, dest string) error { + req, err := http.NewRequestWithContext(ctx, "GET", downloadURL, nil) + if err != nil { + return err + } + + client := &http.Client{Timeout: 10 * time.Minute} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("download returned %d", resp.StatusCode) + } + + // SECURITY: Check Content-Length if available + if resp.ContentLength > maxDownloadSize { + return fmt.Errorf("file too large: %d bytes (max %d)", resp.ContentLength, maxDownloadSize) + } + + out, err := os.Create(dest) + if err != nil { + return err + } + defer out.Close() + + // SECURITY: Use LimitReader to enforce max download size even if Content-Length is missing/wrong + limited := io.LimitReader(resp.Body, maxDownloadSize+1) + written, err := io.Copy(out, limited) + if err != nil { + return err + } + + // Check if we hit the limit (downloaded more than maxDownloadSize) + if written > maxDownloadSize { + os.Remove(dest) // Clean up partial file + return fmt.Errorf("download exceeded maximum size of %d bytes", maxDownloadSize) + } + + return nil +} + +func (s *UpdateService) getArchiveName() string { + osName := runtime.GOOS + arch := runtime.GOARCH + return fmt.Sprintf("%s_%s", osName, arch) +} + +// validateDownloadURL checks if the URL is from an allowed domain +// SECURITY: This prevents SSRF and ensures downloads only come from trusted GitHub domains +func validateDownloadURL(rawURL string) error { + parsedURL, err := url.Parse(rawURL) + if err != nil { + return fmt.Errorf("invalid URL: %w", err) + } + + // Must be HTTPS + if parsedURL.Scheme != "https" { + return fmt.Errorf("only HTTPS URLs are allowed") + } + + // Check against allowed hosts + host := parsedURL.Host + // GitHub release URLs can be from github.com or objects.githubusercontent.com + if host != allowedDownloadHost && + !strings.HasSuffix(host, "."+allowedDownloadHost) && + host != allowedAssetHost && + !strings.HasSuffix(host, "."+allowedAssetHost) { + return fmt.Errorf("download from untrusted host: %s", host) + } + + return nil +} + +func (s *UpdateService) verifyChecksum(ctx context.Context, filePath, checksumURL string) error { + // Download checksums file + req, err := http.NewRequestWithContext(ctx, "GET", checksumURL, nil) + if err != nil { + return err + } + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to download checksums: %d", resp.StatusCode) + } + + // Calculate file hash + f, err := os.Open(filePath) + if err != nil { + return err + } + defer f.Close() + + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return err + } + actualHash := hex.EncodeToString(h.Sum(nil)) + + // Find expected hash in checksums file + fileName := filepath.Base(filePath) + scanner := bufio.NewScanner(resp.Body) + for scanner.Scan() { + line := scanner.Text() + parts := strings.Fields(line) + if len(parts) == 2 && parts[1] == fileName { + if parts[0] == actualHash { + return nil + } + return fmt.Errorf("checksum mismatch: expected %s, got %s", parts[0], actualHash) + } + } + + return fmt.Errorf("checksum not found for %s", fileName) +} + +func (s *UpdateService) extractBinary(archivePath, destPath string) error { + f, err := os.Open(archivePath) + if err != nil { + return err + } + defer f.Close() + + var reader io.Reader = f + + // Handle gzip compression + if strings.HasSuffix(archivePath, ".gz") || strings.HasSuffix(archivePath, ".tar.gz") || strings.HasSuffix(archivePath, ".tgz") { + gzr, err := gzip.NewReader(f) + if err != nil { + return err + } + defer gzr.Close() + reader = gzr + } + + // Handle tar archive + if strings.Contains(archivePath, ".tar") { + tr := tar.NewReader(reader) + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + + // SECURITY: Prevent Zip Slip / Path Traversal attack + // Only allow files with safe base names, no directory traversal + baseName := filepath.Base(hdr.Name) + + // Check for path traversal attempts + if strings.Contains(hdr.Name, "..") { + return fmt.Errorf("path traversal attempt detected: %s", hdr.Name) + } + + // Validate the entry is a regular file + if hdr.Typeflag != tar.TypeReg { + continue // Skip directories and special files + } + + // Only extract the specific binary we need + if baseName == "sub2api" || baseName == "sub2api.exe" { + // Additional security: limit file size (max 500MB) + const maxBinarySize = 500 * 1024 * 1024 + if hdr.Size > maxBinarySize { + return fmt.Errorf("binary too large: %d bytes (max %d)", hdr.Size, maxBinarySize) + } + + out, err := os.Create(destPath) + if err != nil { + return err + } + + // Use LimitReader to prevent decompression bombs + limited := io.LimitReader(tr, maxBinarySize) + if _, err := io.Copy(out, limited); err != nil { + out.Close() + return err + } + out.Close() + return nil + } + } + return fmt.Errorf("binary not found in archive") + } + + // Direct copy for non-tar files (with size limit) + const maxBinarySize = 500 * 1024 * 1024 + out, err := os.Create(destPath) + if err != nil { + return err + } + defer out.Close() + + limited := io.LimitReader(reader, maxBinarySize) + _, err = io.Copy(out, limited) + return err +} + +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, in) + return err +} + +func (s *UpdateService) getFromCache(ctx context.Context) (*UpdateInfo, error) { + data, err := s.rdb.Get(ctx, updateCacheKey).Result() + if err != nil { + return nil, err + } + + var cached struct { + Latest string `json:"latest"` + ReleaseInfo *ReleaseInfo `json:"release_info"` + Timestamp int64 `json:"timestamp"` + } + if err := json.Unmarshal([]byte(data), &cached); err != nil { + return nil, err + } + + if time.Now().Unix()-cached.Timestamp > updateCacheTTL { + return nil, fmt.Errorf("cache expired") + } + + return &UpdateInfo{ + CurrentVersion: s.currentVersion, + LatestVersion: cached.Latest, + HasUpdate: compareVersions(s.currentVersion, cached.Latest) < 0, + ReleaseInfo: cached.ReleaseInfo, + Cached: true, + BuildType: s.buildType, + }, nil +} + +func (s *UpdateService) saveToCache(ctx context.Context, info *UpdateInfo) { + cacheData := struct { + Latest string `json:"latest"` + ReleaseInfo *ReleaseInfo `json:"release_info"` + Timestamp int64 `json:"timestamp"` + }{ + Latest: info.LatestVersion, + ReleaseInfo: info.ReleaseInfo, + Timestamp: time.Now().Unix(), + } + + data, _ := json.Marshal(cacheData) + s.rdb.Set(ctx, updateCacheKey, data, time.Duration(updateCacheTTL)*time.Second) +} + +// compareVersions compares two semantic versions +func compareVersions(current, latest string) int { + currentParts := parseVersion(current) + latestParts := parseVersion(latest) + + for i := 0; i < 3; i++ { + if currentParts[i] < latestParts[i] { + return -1 + } + if currentParts[i] > latestParts[i] { + return 1 + } + } + return 0 +} + +func parseVersion(v string) [3]int { + v = strings.TrimPrefix(v, "v") + parts := strings.Split(v, ".") + result := [3]int{0, 0, 0} + for i := 0; i < len(parts) && i < 3; i++ { + fmt.Sscanf(parts[i], "%d", &result[i]) + } + return result +} diff --git a/backend/internal/service/usage_service.go b/backend/internal/service/usage_service.go new file mode 100644 index 00000000..214ba26a --- /dev/null +++ b/backend/internal/service/usage_service.go @@ -0,0 +1,283 @@ +package service + +import ( + "context" + "errors" + "fmt" + "sub2api/internal/model" + "sub2api/internal/repository" + "time" + + "gorm.io/gorm" +) + +var ( + ErrUsageLogNotFound = errors.New("usage log not found") +) + +// CreateUsageLogRequest 创建使用日志请求 +type CreateUsageLogRequest struct { + UserID int64 `json:"user_id"` + ApiKeyID int64 `json:"api_key_id"` + AccountID int64 `json:"account_id"` + RequestID string `json:"request_id"` + Model string `json:"model"` + InputTokens int `json:"input_tokens"` + OutputTokens int `json:"output_tokens"` + CacheCreationTokens int `json:"cache_creation_tokens"` + CacheReadTokens int `json:"cache_read_tokens"` + CacheCreation5mTokens int `json:"cache_creation_5m_tokens"` + CacheCreation1hTokens int `json:"cache_creation_1h_tokens"` + InputCost float64 `json:"input_cost"` + OutputCost float64 `json:"output_cost"` + CacheCreationCost float64 `json:"cache_creation_cost"` + CacheReadCost float64 `json:"cache_read_cost"` + TotalCost float64 `json:"total_cost"` + ActualCost float64 `json:"actual_cost"` + RateMultiplier float64 `json:"rate_multiplier"` + Stream bool `json:"stream"` + DurationMs *int `json:"duration_ms"` +} + +// UsageStats 使用统计 +type UsageStats struct { + TotalRequests int64 `json:"total_requests"` + TotalInputTokens int64 `json:"total_input_tokens"` + TotalOutputTokens int64 `json:"total_output_tokens"` + TotalCacheTokens int64 `json:"total_cache_tokens"` + TotalTokens int64 `json:"total_tokens"` + TotalCost float64 `json:"total_cost"` + TotalActualCost float64 `json:"total_actual_cost"` + AverageDurationMs float64 `json:"average_duration_ms"` +} + +// UsageService 使用统计服务 +type UsageService struct { + usageRepo *repository.UsageLogRepository + userRepo *repository.UserRepository +} + +// NewUsageService 创建使用统计服务实例 +func NewUsageService(usageRepo *repository.UsageLogRepository, userRepo *repository.UserRepository) *UsageService { + return &UsageService{ + usageRepo: usageRepo, + userRepo: userRepo, + } +} + +// Create 创建使用日志 +func (s *UsageService) Create(ctx context.Context, req CreateUsageLogRequest) (*model.UsageLog, error) { + // 验证用户存在 + _, err := s.userRepo.GetByID(ctx, req.UserID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrUserNotFound + } + return nil, fmt.Errorf("get user: %w", err) + } + + // 创建使用日志 + usageLog := &model.UsageLog{ + UserID: req.UserID, + ApiKeyID: req.ApiKeyID, + AccountID: req.AccountID, + RequestID: req.RequestID, + Model: req.Model, + InputTokens: req.InputTokens, + OutputTokens: req.OutputTokens, + CacheCreationTokens: req.CacheCreationTokens, + CacheReadTokens: req.CacheReadTokens, + CacheCreation5mTokens: req.CacheCreation5mTokens, + CacheCreation1hTokens: req.CacheCreation1hTokens, + InputCost: req.InputCost, + OutputCost: req.OutputCost, + CacheCreationCost: req.CacheCreationCost, + CacheReadCost: req.CacheReadCost, + TotalCost: req.TotalCost, + ActualCost: req.ActualCost, + RateMultiplier: req.RateMultiplier, + Stream: req.Stream, + DurationMs: req.DurationMs, + } + + if err := s.usageRepo.Create(ctx, usageLog); err != nil { + return nil, fmt.Errorf("create usage log: %w", err) + } + + // 扣除用户余额 + if req.ActualCost > 0 { + if err := s.userRepo.UpdateBalance(ctx, req.UserID, -req.ActualCost); err != nil { + return nil, fmt.Errorf("update user balance: %w", err) + } + } + + return usageLog, nil +} + +// GetByID 根据ID获取使用日志 +func (s *UsageService) GetByID(ctx context.Context, id int64) (*model.UsageLog, error) { + log, err := s.usageRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrUsageLogNotFound + } + return nil, fmt.Errorf("get usage log: %w", err) + } + return log, nil +} + +// ListByUser 获取用户的使用日志列表 +func (s *UsageService) ListByUser(ctx context.Context, userID int64, params repository.PaginationParams) ([]model.UsageLog, *repository.PaginationResult, error) { + logs, pagination, err := s.usageRepo.ListByUser(ctx, userID, params) + if err != nil { + return nil, nil, fmt.Errorf("list usage logs: %w", err) + } + return logs, pagination, nil +} + +// ListByApiKey 获取API Key的使用日志列表 +func (s *UsageService) ListByApiKey(ctx context.Context, apiKeyID int64, params repository.PaginationParams) ([]model.UsageLog, *repository.PaginationResult, error) { + logs, pagination, err := s.usageRepo.ListByApiKey(ctx, apiKeyID, params) + if err != nil { + return nil, nil, fmt.Errorf("list usage logs: %w", err) + } + return logs, pagination, nil +} + +// ListByAccount 获取账号的使用日志列表 +func (s *UsageService) ListByAccount(ctx context.Context, accountID int64, params repository.PaginationParams) ([]model.UsageLog, *repository.PaginationResult, error) { + logs, pagination, err := s.usageRepo.ListByAccount(ctx, accountID, params) + if err != nil { + return nil, nil, fmt.Errorf("list usage logs: %w", err) + } + return logs, pagination, nil +} + +// GetStatsByUser 获取用户的使用统计 +func (s *UsageService) GetStatsByUser(ctx context.Context, userID int64, startTime, endTime time.Time) (*UsageStats, error) { + logs, _, err := s.usageRepo.ListByUserAndTimeRange(ctx, userID, startTime, endTime) + if err != nil { + return nil, fmt.Errorf("list usage logs: %w", err) + } + + return s.calculateStats(logs), nil +} + +// GetStatsByApiKey 获取API Key的使用统计 +func (s *UsageService) GetStatsByApiKey(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) (*UsageStats, error) { + logs, _, err := s.usageRepo.ListByApiKeyAndTimeRange(ctx, apiKeyID, startTime, endTime) + if err != nil { + return nil, fmt.Errorf("list usage logs: %w", err) + } + + return s.calculateStats(logs), nil +} + +// GetStatsByAccount 获取账号的使用统计 +func (s *UsageService) GetStatsByAccount(ctx context.Context, accountID int64, startTime, endTime time.Time) (*UsageStats, error) { + logs, _, err := s.usageRepo.ListByAccountAndTimeRange(ctx, accountID, startTime, endTime) + if err != nil { + return nil, fmt.Errorf("list usage logs: %w", err) + } + + return s.calculateStats(logs), nil +} + +// GetStatsByModel 获取模型的使用统计 +func (s *UsageService) GetStatsByModel(ctx context.Context, modelName string, startTime, endTime time.Time) (*UsageStats, error) { + logs, _, err := s.usageRepo.ListByModelAndTimeRange(ctx, modelName, startTime, endTime) + if err != nil { + return nil, fmt.Errorf("list usage logs: %w", err) + } + + return s.calculateStats(logs), nil +} + +// GetDailyStats 获取每日使用统计(最近N天) +func (s *UsageService) GetDailyStats(ctx context.Context, userID int64, days int) ([]map[string]interface{}, error) { + endTime := time.Now() + startTime := endTime.AddDate(0, 0, -days) + + logs, _, err := s.usageRepo.ListByUserAndTimeRange(ctx, userID, startTime, endTime) + if err != nil { + return nil, fmt.Errorf("list usage logs: %w", err) + } + + // 按日期分组统计 + dailyStats := make(map[string]*UsageStats) + for _, log := range logs { + dateKey := log.CreatedAt.Format("2006-01-02") + if _, exists := dailyStats[dateKey]; !exists { + dailyStats[dateKey] = &UsageStats{} + } + + stats := dailyStats[dateKey] + stats.TotalRequests++ + stats.TotalInputTokens += int64(log.InputTokens) + stats.TotalOutputTokens += int64(log.OutputTokens) + stats.TotalCacheTokens += int64(log.CacheCreationTokens + log.CacheReadTokens) + stats.TotalTokens += int64(log.TotalTokens()) + stats.TotalCost += log.TotalCost + stats.TotalActualCost += log.ActualCost + + if log.DurationMs != nil { + stats.AverageDurationMs += float64(*log.DurationMs) + } + } + + // 计算平均值并转换为数组 + result := make([]map[string]interface{}, 0, len(dailyStats)) + for date, stats := range dailyStats { + if stats.TotalRequests > 0 { + stats.AverageDurationMs /= float64(stats.TotalRequests) + } + + result = append(result, map[string]interface{}{ + "date": date, + "total_requests": stats.TotalRequests, + "total_input_tokens": stats.TotalInputTokens, + "total_output_tokens": stats.TotalOutputTokens, + "total_cache_tokens": stats.TotalCacheTokens, + "total_tokens": stats.TotalTokens, + "total_cost": stats.TotalCost, + "total_actual_cost": stats.TotalActualCost, + "average_duration_ms": stats.AverageDurationMs, + }) + } + + return result, nil +} + +// calculateStats 计算统计数据 +func (s *UsageService) calculateStats(logs []model.UsageLog) *UsageStats { + stats := &UsageStats{} + + for _, log := range logs { + stats.TotalRequests++ + stats.TotalInputTokens += int64(log.InputTokens) + stats.TotalOutputTokens += int64(log.OutputTokens) + stats.TotalCacheTokens += int64(log.CacheCreationTokens + log.CacheReadTokens) + stats.TotalTokens += int64(log.TotalTokens()) + stats.TotalCost += log.TotalCost + stats.TotalActualCost += log.ActualCost + + if log.DurationMs != nil { + stats.AverageDurationMs += float64(*log.DurationMs) + } + } + + // 计算平均持续时间 + if stats.TotalRequests > 0 { + stats.AverageDurationMs /= float64(stats.TotalRequests) + } + + return stats +} + +// Delete 删除使用日志(管理员功能,谨慎使用) +func (s *UsageService) Delete(ctx context.Context, id int64) error { + if err := s.usageRepo.Delete(ctx, id); err != nil { + return fmt.Errorf("delete usage log: %w", err) + } + return nil +} diff --git a/backend/internal/service/user_service.go b/backend/internal/service/user_service.go new file mode 100644 index 00000000..bc0af756 --- /dev/null +++ b/backend/internal/service/user_service.go @@ -0,0 +1,177 @@ +package service + +import ( + "context" + "errors" + "fmt" + "sub2api/internal/config" + "sub2api/internal/model" + "sub2api/internal/repository" + + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +var ( + ErrUserNotFound = errors.New("user not found") + ErrPasswordIncorrect = errors.New("current password is incorrect") + ErrInsufficientPerms = errors.New("insufficient permissions") +) + +// UpdateProfileRequest 更新用户资料请求 +type UpdateProfileRequest struct { + Email *string `json:"email"` + Concurrency *int `json:"concurrency"` +} + +// ChangePasswordRequest 修改密码请求 +type ChangePasswordRequest struct { + CurrentPassword string `json:"current_password"` + NewPassword string `json:"new_password"` +} + +// UserService 用户服务 +type UserService struct { + userRepo *repository.UserRepository + cfg *config.Config +} + +// NewUserService 创建用户服务实例 +func NewUserService(userRepo *repository.UserRepository, cfg *config.Config) *UserService { + return &UserService{ + userRepo: userRepo, + cfg: cfg, + } +} + +// GetProfile 获取用户资料 +func (s *UserService) GetProfile(ctx context.Context, userID int64) (*model.User, error) { + user, err := s.userRepo.GetByID(ctx, userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrUserNotFound + } + return nil, fmt.Errorf("get user: %w", err) + } + return user, nil +} + +// UpdateProfile 更新用户资料 +func (s *UserService) UpdateProfile(ctx context.Context, userID int64, req UpdateProfileRequest) (*model.User, error) { + user, err := s.userRepo.GetByID(ctx, userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrUserNotFound + } + return nil, fmt.Errorf("get user: %w", err) + } + + // 更新字段 + if req.Email != nil { + // 检查新邮箱是否已被使用 + exists, err := s.userRepo.ExistsByEmail(ctx, *req.Email) + if err != nil { + return nil, fmt.Errorf("check email exists: %w", err) + } + if exists && *req.Email != user.Email { + return nil, ErrEmailExists + } + user.Email = *req.Email + } + + if req.Concurrency != nil { + user.Concurrency = *req.Concurrency + } + + if err := s.userRepo.Update(ctx, user); err != nil { + return nil, fmt.Errorf("update user: %w", err) + } + + return user, nil +} + +// ChangePassword 修改密码 +func (s *UserService) ChangePassword(ctx context.Context, userID int64, req ChangePasswordRequest) error { + user, err := s.userRepo.GetByID(ctx, userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrUserNotFound + } + return fmt.Errorf("get user: %w", err) + } + + // 验证当前密码 + if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.CurrentPassword)); err != nil { + return ErrPasswordIncorrect + } + + // 生成新密码哈希 + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost) + if err != nil { + return fmt.Errorf("hash password: %w", err) + } + + user.PasswordHash = string(hashedPassword) + + if err := s.userRepo.Update(ctx, user); err != nil { + return fmt.Errorf("update user: %w", err) + } + + return nil +} + +// GetByID 根据ID获取用户(管理员功能) +func (s *UserService) GetByID(ctx context.Context, id int64) (*model.User, error) { + user, err := s.userRepo.GetByID(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrUserNotFound + } + return nil, fmt.Errorf("get user: %w", err) + } + return user, nil +} + +// List 获取用户列表(管理员功能) +func (s *UserService) List(ctx context.Context, params repository.PaginationParams) ([]model.User, *repository.PaginationResult, error) { + users, pagination, err := s.userRepo.List(ctx, params) + if err != nil { + return nil, nil, fmt.Errorf("list users: %w", err) + } + return users, pagination, nil +} + +// UpdateBalance 更新用户余额(管理员功能) +func (s *UserService) UpdateBalance(ctx context.Context, userID int64, amount float64) error { + if err := s.userRepo.UpdateBalance(ctx, userID, amount); err != nil { + return fmt.Errorf("update balance: %w", err) + } + return nil +} + +// UpdateStatus 更新用户状态(管理员功能) +func (s *UserService) UpdateStatus(ctx context.Context, userID int64, status string) error { + user, err := s.userRepo.GetByID(ctx, userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrUserNotFound + } + return fmt.Errorf("get user: %w", err) + } + + user.Status = status + + if err := s.userRepo.Update(ctx, user); err != nil { + return fmt.Errorf("update user: %w", err) + } + + return nil +} + +// Delete 删除用户(管理员功能) +func (s *UserService) Delete(ctx context.Context, userID int64) error { + if err := s.userRepo.Delete(ctx, userID); err != nil { + return fmt.Errorf("delete user: %w", err) + } + return nil +} diff --git a/backend/internal/setup/cli.go b/backend/internal/setup/cli.go new file mode 100644 index 00000000..0d57d93f --- /dev/null +++ b/backend/internal/setup/cli.go @@ -0,0 +1,294 @@ +package setup + +import ( + "bufio" + "fmt" + "net/mail" + "os" + "regexp" + "strconv" + "strings" + + "golang.org/x/term" +) + +// CLI input validation functions (matching Web API validation) +func cliValidateHostname(host string) bool { + validHost := regexp.MustCompile(`^[a-zA-Z0-9.\-:]+$`) + return validHost.MatchString(host) && len(host) <= 253 +} + +func cliValidateDBName(name string) bool { + validName := regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]*$`) + return validName.MatchString(name) && len(name) <= 63 +} + +func cliValidateUsername(name string) bool { + validName := regexp.MustCompile(`^[a-zA-Z0-9_]+$`) + return validName.MatchString(name) && len(name) <= 63 +} + +func cliValidateEmail(email string) bool { + _, err := mail.ParseAddress(email) + return err == nil && len(email) <= 254 +} + +func cliValidatePort(port int) bool { + return port > 0 && port <= 65535 +} + +func cliValidateSSLMode(mode string) bool { + validModes := map[string]bool{ + "disable": true, "require": true, "verify-ca": true, "verify-full": true, + } + return validModes[mode] +} + +// RunCLI runs the CLI setup wizard +func RunCLI() error { + reader := bufio.NewReader(os.Stdin) + + fmt.Println() + fmt.Println("╔═══════════════════════════════════════════╗") + fmt.Println("║ Sub2API Installation Wizard ║") + fmt.Println("╚═══════════════════════════════════════════╝") + fmt.Println() + + cfg := &SetupConfig{ + Server: ServerConfig{ + Host: "0.0.0.0", + Port: 8080, + Mode: "release", + }, + JWT: JWTConfig{ + ExpireHour: 24, + }, + } + + // Database configuration with validation + fmt.Println("── Database Configuration ──") + + for { + cfg.Database.Host = promptString(reader, "PostgreSQL Host", "localhost") + if cliValidateHostname(cfg.Database.Host) { + break + } + fmt.Println(" Invalid hostname format. Use alphanumeric, dots, hyphens only.") + } + + for { + cfg.Database.Port = promptInt(reader, "PostgreSQL Port", 5432) + if cliValidatePort(cfg.Database.Port) { + break + } + fmt.Println(" Invalid port. Must be between 1 and 65535.") + } + + for { + cfg.Database.User = promptString(reader, "PostgreSQL User", "postgres") + if cliValidateUsername(cfg.Database.User) { + break + } + fmt.Println(" Invalid username. Use alphanumeric and underscores only.") + } + + cfg.Database.Password = promptPassword("PostgreSQL Password") + + for { + cfg.Database.DBName = promptString(reader, "Database Name", "sub2api") + if cliValidateDBName(cfg.Database.DBName) { + break + } + fmt.Println(" Invalid database name. Start with letter, use alphanumeric and underscores.") + } + + for { + cfg.Database.SSLMode = promptString(reader, "SSL Mode", "disable") + if cliValidateSSLMode(cfg.Database.SSLMode) { + break + } + fmt.Println(" Invalid SSL mode. Use: disable, require, verify-ca, or verify-full.") + } + + fmt.Println() + fmt.Print("Testing database connection... ") + if err := TestDatabaseConnection(&cfg.Database); err != nil { + fmt.Println("FAILED") + return fmt.Errorf("database connection failed: %w", err) + } + fmt.Println("OK") + + // Redis configuration with validation + fmt.Println() + fmt.Println("── Redis Configuration ──") + + for { + cfg.Redis.Host = promptString(reader, "Redis Host", "localhost") + if cliValidateHostname(cfg.Redis.Host) { + break + } + fmt.Println(" Invalid hostname format. Use alphanumeric, dots, hyphens only.") + } + + for { + cfg.Redis.Port = promptInt(reader, "Redis Port", 6379) + if cliValidatePort(cfg.Redis.Port) { + break + } + fmt.Println(" Invalid port. Must be between 1 and 65535.") + } + + cfg.Redis.Password = promptPassword("Redis Password (optional)") + + for { + cfg.Redis.DB = promptInt(reader, "Redis DB", 0) + if cfg.Redis.DB >= 0 && cfg.Redis.DB <= 15 { + break + } + fmt.Println(" Invalid Redis DB. Must be between 0 and 15.") + } + + fmt.Println() + fmt.Print("Testing Redis connection... ") + if err := TestRedisConnection(&cfg.Redis); err != nil { + fmt.Println("FAILED") + return fmt.Errorf("redis connection failed: %w", err) + } + fmt.Println("OK") + + // Admin configuration with validation + fmt.Println() + fmt.Println("── Admin Account ──") + + for { + cfg.Admin.Email = promptString(reader, "Admin Email", "admin@example.com") + if cliValidateEmail(cfg.Admin.Email) { + break + } + fmt.Println(" Invalid email format.") + } + + for { + cfg.Admin.Password = promptPassword("Admin Password") + // SECURITY: Match Web API requirement of 8 characters minimum + if len(cfg.Admin.Password) < 8 { + fmt.Println(" Password must be at least 8 characters") + continue + } + if len(cfg.Admin.Password) > 128 { + fmt.Println(" Password must be at most 128 characters") + continue + } + confirm := promptPassword("Confirm Password") + if cfg.Admin.Password != confirm { + fmt.Println(" Passwords do not match") + continue + } + break + } + + // Server configuration with validation + fmt.Println() + fmt.Println("── Server Configuration ──") + + for { + cfg.Server.Port = promptInt(reader, "Server Port", 8080) + if cliValidatePort(cfg.Server.Port) { + break + } + fmt.Println(" Invalid port. Must be between 1 and 65535.") + } + + // Confirm and install + fmt.Println() + fmt.Println("── Configuration Summary ──") + fmt.Printf("Database: %s@%s:%d/%s\n", cfg.Database.User, cfg.Database.Host, cfg.Database.Port, cfg.Database.DBName) + fmt.Printf("Redis: %s:%d\n", cfg.Redis.Host, cfg.Redis.Port) + fmt.Printf("Admin: %s\n", cfg.Admin.Email) + fmt.Printf("Server: :%d\n", cfg.Server.Port) + fmt.Println() + + if !promptConfirm(reader, "Proceed with installation?") { + fmt.Println("Installation cancelled") + return nil + } + + fmt.Println() + fmt.Print("Installing... ") + if err := Install(cfg); err != nil { + fmt.Println("FAILED") + return err + } + fmt.Println("OK") + + fmt.Println() + fmt.Println("╔═══════════════════════════════════════════╗") + fmt.Println("║ Installation Complete! ║") + fmt.Println("╚═══════════════════════════════════════════╝") + fmt.Println() + fmt.Println("Start the server with:") + fmt.Println(" ./sub2api") + fmt.Println() + fmt.Printf("Admin panel: http://localhost:%d\n", cfg.Server.Port) + fmt.Println() + + return nil +} + +func promptString(reader *bufio.Reader, prompt, defaultVal string) string { + if defaultVal != "" { + fmt.Printf(" %s [%s]: ", prompt, defaultVal) + } else { + fmt.Printf(" %s: ", prompt) + } + + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(input) + + if input == "" { + return defaultVal + } + return input +} + +func promptInt(reader *bufio.Reader, prompt string, defaultVal int) int { + fmt.Printf(" %s [%d]: ", prompt, defaultVal) + + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(input) + + if input == "" { + return defaultVal + } + + val, err := strconv.Atoi(input) + if err != nil { + return defaultVal + } + return val +} + +func promptPassword(prompt string) string { + fmt.Printf(" %s: ", prompt) + + // Try to read password without echo + if term.IsTerminal(int(os.Stdin.Fd())) { + password, err := term.ReadPassword(int(os.Stdin.Fd())) + fmt.Println() + if err == nil { + return string(password) + } + } + + // Fallback to regular input + reader := bufio.NewReader(os.Stdin) + input, _ := reader.ReadString('\n') + return strings.TrimSpace(input) +} + +func promptConfirm(reader *bufio.Reader, prompt string) bool { + fmt.Printf("%s [y/N]: ", prompt) + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(strings.ToLower(input)) + return input == "y" || input == "yes" +} diff --git a/backend/internal/setup/handler.go b/backend/internal/setup/handler.go new file mode 100644 index 00000000..5565539d --- /dev/null +++ b/backend/internal/setup/handler.go @@ -0,0 +1,344 @@ +package setup + +import ( + "fmt" + "net/http" + "net/mail" + "regexp" + "strings" + "sync" + + "sub2api/internal/pkg/response" + + "github.com/gin-gonic/gin" +) + +// installMutex prevents concurrent installation attempts (TOCTOU protection) +var installMutex sync.Mutex + +// RegisterRoutes registers setup wizard routes +func RegisterRoutes(r *gin.Engine) { + setup := r.Group("/setup") + { + // Status endpoint is always accessible (read-only) + setup.GET("/status", getStatus) + + // All modification endpoints are protected by setupGuard + protected := setup.Group("") + protected.Use(setupGuard()) + { + protected.POST("/test-db", testDatabase) + protected.POST("/test-redis", testRedis) + protected.POST("/install", install) + } + } +} + +// SetupStatus represents the current setup state +type SetupStatus struct { + NeedsSetup bool `json:"needs_setup"` + Step string `json:"step"` +} + +// getStatus returns the current setup status +func getStatus(c *gin.Context) { + response.Success(c, SetupStatus{ + NeedsSetup: NeedsSetup(), + Step: "welcome", + }) +} + +// setupGuard middleware ensures setup endpoints are only accessible during setup mode +func setupGuard() gin.HandlerFunc { + return func(c *gin.Context) { + if !NeedsSetup() { + response.Error(c, http.StatusForbidden, "Setup is not allowed: system is already installed") + c.Abort() + return + } + c.Next() + } +} + +// validateHostname checks if a hostname/IP is safe (no injection characters) +func validateHostname(host string) bool { + // Allow only alphanumeric, dots, hyphens, and colons (for IPv6) + validHost := regexp.MustCompile(`^[a-zA-Z0-9.\-:]+$`) + return validHost.MatchString(host) && len(host) <= 253 +} + +// validateDBName checks if database name is safe +func validateDBName(name string) bool { + // Allow only alphanumeric and underscores, starting with letter + validName := regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]*$`) + return validName.MatchString(name) && len(name) <= 63 +} + +// validateUsername checks if username is safe +func validateUsername(name string) bool { + // Allow only alphanumeric and underscores + validName := regexp.MustCompile(`^[a-zA-Z0-9_]+$`) + return validName.MatchString(name) && len(name) <= 63 +} + +// validateEmail checks if email format is valid +func validateEmail(email string) bool { + _, err := mail.ParseAddress(email) + return err == nil && len(email) <= 254 +} + +// validatePassword checks password strength +func validatePassword(password string) error { + if len(password) < 8 { + return fmt.Errorf("password must be at least 8 characters") + } + if len(password) > 128 { + return fmt.Errorf("password must be at most 128 characters") + } + return nil +} + +// validatePort checks if port is in valid range +func validatePort(port int) bool { + return port > 0 && port <= 65535 +} + +// validateSSLMode checks if SSL mode is valid +func validateSSLMode(mode string) bool { + validModes := map[string]bool{ + "disable": true, "require": true, "verify-ca": true, "verify-full": true, + } + return validModes[mode] +} + +// TestDatabaseRequest represents database test request +type TestDatabaseRequest struct { + Host string `json:"host" binding:"required"` + Port int `json:"port" binding:"required"` + User string `json:"user" binding:"required"` + Password string `json:"password"` + DBName string `json:"dbname" binding:"required"` + SSLMode string `json:"sslmode"` +} + +// testDatabase tests database connection +func testDatabase(c *gin.Context) { + var req TestDatabaseRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.Error(c, http.StatusBadRequest, "Invalid request: "+err.Error()) + return + } + + // Security: Validate all inputs to prevent injection attacks + if !validateHostname(req.Host) { + response.Error(c, http.StatusBadRequest, "Invalid hostname format") + return + } + if !validatePort(req.Port) { + response.Error(c, http.StatusBadRequest, "Invalid port number") + return + } + if !validateUsername(req.User) { + response.Error(c, http.StatusBadRequest, "Invalid username format") + return + } + if !validateDBName(req.DBName) { + response.Error(c, http.StatusBadRequest, "Invalid database name format") + return + } + + if req.SSLMode == "" { + req.SSLMode = "disable" + } + if !validateSSLMode(req.SSLMode) { + response.Error(c, http.StatusBadRequest, "Invalid SSL mode") + return + } + + cfg := &DatabaseConfig{ + Host: req.Host, + Port: req.Port, + User: req.User, + Password: req.Password, + DBName: req.DBName, + SSLMode: req.SSLMode, + } + + if err := TestDatabaseConnection(cfg); err != nil { + response.Error(c, http.StatusBadRequest, "Connection failed: "+err.Error()) + return + } + + response.Success(c, gin.H{"message": "Connection successful"}) +} + +// TestRedisRequest represents Redis test request +type TestRedisRequest struct { + Host string `json:"host" binding:"required"` + Port int `json:"port" binding:"required"` + Password string `json:"password"` + DB int `json:"db"` +} + +// testRedis tests Redis connection +func testRedis(c *gin.Context) { + var req TestRedisRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.Error(c, http.StatusBadRequest, "Invalid request: "+err.Error()) + return + } + + // Security: Validate inputs + if !validateHostname(req.Host) { + response.Error(c, http.StatusBadRequest, "Invalid hostname format") + return + } + if !validatePort(req.Port) { + response.Error(c, http.StatusBadRequest, "Invalid port number") + return + } + if req.DB < 0 || req.DB > 15 { + response.Error(c, http.StatusBadRequest, "Invalid Redis database number (0-15)") + return + } + + cfg := &RedisConfig{ + Host: req.Host, + Port: req.Port, + Password: req.Password, + DB: req.DB, + } + + if err := TestRedisConnection(cfg); err != nil { + response.Error(c, http.StatusBadRequest, "Connection failed: "+err.Error()) + return + } + + response.Success(c, gin.H{"message": "Connection successful"}) +} + +// InstallRequest represents installation request +type InstallRequest struct { + Database DatabaseConfig `json:"database" binding:"required"` + Redis RedisConfig `json:"redis" binding:"required"` + Admin AdminConfig `json:"admin" binding:"required"` + Server ServerConfig `json:"server"` +} + +// install performs the installation +func install(c *gin.Context) { + // TOCTOU Protection: Acquire mutex to prevent concurrent installation + installMutex.Lock() + defer installMutex.Unlock() + + // Double-check after acquiring lock + if !NeedsSetup() { + response.Error(c, http.StatusForbidden, "Setup is not allowed: system is already installed") + return + } + + var req InstallRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.Error(c, http.StatusBadRequest, "Invalid request: "+err.Error()) + return + } + + // ========== COMPREHENSIVE INPUT VALIDATION ========== + // Database validation + if !validateHostname(req.Database.Host) { + response.Error(c, http.StatusBadRequest, "Invalid database hostname") + return + } + if !validatePort(req.Database.Port) { + response.Error(c, http.StatusBadRequest, "Invalid database port") + return + } + if !validateUsername(req.Database.User) { + response.Error(c, http.StatusBadRequest, "Invalid database username") + return + } + if !validateDBName(req.Database.DBName) { + response.Error(c, http.StatusBadRequest, "Invalid database name") + return + } + + // Redis validation + if !validateHostname(req.Redis.Host) { + response.Error(c, http.StatusBadRequest, "Invalid Redis hostname") + return + } + if !validatePort(req.Redis.Port) { + response.Error(c, http.StatusBadRequest, "Invalid Redis port") + return + } + if req.Redis.DB < 0 || req.Redis.DB > 15 { + response.Error(c, http.StatusBadRequest, "Invalid Redis database number") + return + } + + // Admin validation + if !validateEmail(req.Admin.Email) { + response.Error(c, http.StatusBadRequest, "Invalid admin email format") + return + } + if err := validatePassword(req.Admin.Password); err != nil { + response.Error(c, http.StatusBadRequest, err.Error()) + return + } + + // Server validation + if req.Server.Port != 0 && !validatePort(req.Server.Port) { + response.Error(c, http.StatusBadRequest, "Invalid server port") + return + } + + // ========== SET DEFAULTS ========== + if req.Database.SSLMode == "" { + req.Database.SSLMode = "disable" + } + if !validateSSLMode(req.Database.SSLMode) { + response.Error(c, http.StatusBadRequest, "Invalid SSL mode") + return + } + if req.Server.Host == "" { + req.Server.Host = "0.0.0.0" + } + if req.Server.Port == 0 { + req.Server.Port = 8080 + } + if req.Server.Mode == "" { + req.Server.Mode = "release" + } + // Validate server mode + if req.Server.Mode != "release" && req.Server.Mode != "debug" { + response.Error(c, http.StatusBadRequest, "Invalid server mode (must be 'release' or 'debug')") + return + } + + // Trim whitespace from string inputs + req.Admin.Email = strings.TrimSpace(req.Admin.Email) + req.Database.Host = strings.TrimSpace(req.Database.Host) + req.Database.User = strings.TrimSpace(req.Database.User) + req.Database.DBName = strings.TrimSpace(req.Database.DBName) + req.Redis.Host = strings.TrimSpace(req.Redis.Host) + + cfg := &SetupConfig{ + Database: req.Database, + Redis: req.Redis, + Admin: req.Admin, + Server: req.Server, + JWT: JWTConfig{ + ExpireHour: 24, + }, + } + + if err := Install(cfg); err != nil { + response.Error(c, http.StatusInternalServerError, "Installation failed: "+err.Error()) + return + } + + response.Success(c, gin.H{ + "message": "Installation completed successfully", + "restart": true, + }) +} diff --git a/backend/internal/setup/setup.go b/backend/internal/setup/setup.go new file mode 100644 index 00000000..a7d062da --- /dev/null +++ b/backend/internal/setup/setup.go @@ -0,0 +1,564 @@ +package setup + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "log" + "os" + "strconv" + "time" + + "github.com/redis/go-redis/v9" + "golang.org/x/crypto/bcrypt" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gopkg.in/yaml.v3" +) + +// Config paths +const ( + ConfigFile = "config.yaml" + EnvFile = ".env" +) + +// SetupConfig holds the setup configuration +type SetupConfig struct { + Database DatabaseConfig `json:"database" yaml:"database"` + Redis RedisConfig `json:"redis" yaml:"redis"` + Admin AdminConfig `json:"admin" yaml:"-"` // Not stored in config file + Server ServerConfig `json:"server" yaml:"server"` + JWT JWTConfig `json:"jwt" yaml:"jwt"` + Timezone string `json:"timezone" yaml:"timezone"` // e.g. "Asia/Shanghai", "UTC" +} + +type DatabaseConfig struct { + Host string `json:"host" yaml:"host"` + Port int `json:"port" yaml:"port"` + User string `json:"user" yaml:"user"` + Password string `json:"password" yaml:"password"` + DBName string `json:"dbname" yaml:"dbname"` + SSLMode string `json:"sslmode" yaml:"sslmode"` +} + +type RedisConfig struct { + Host string `json:"host" yaml:"host"` + Port int `json:"port" yaml:"port"` + Password string `json:"password" yaml:"password"` + DB int `json:"db" yaml:"db"` +} + +type AdminConfig struct { + Email string `json:"email"` + Password string `json:"password"` +} + +type ServerConfig struct { + Host string `json:"host" yaml:"host"` + Port int `json:"port" yaml:"port"` + Mode string `json:"mode" yaml:"mode"` +} + +type JWTConfig struct { + Secret string `json:"secret" yaml:"secret"` + ExpireHour int `json:"expire_hour" yaml:"expire_hour"` +} + +// NeedsSetup checks if the system needs initial setup +// Uses multiple checks to prevent attackers from forcing re-setup by deleting config +func NeedsSetup() bool { + // Check 1: Config file must not exist + if _, err := os.Stat(ConfigFile); !os.IsNotExist(err) { + return false // Config exists, no setup needed + } + + // Check 2: Installation lock file (harder to bypass) + lockFile := ".installed" + if _, err := os.Stat(lockFile); !os.IsNotExist(err) { + return false // Lock file exists, already installed + } + + return true +} + +// TestDatabaseConnection tests the database connection +func TestDatabaseConnection(cfg *DatabaseConfig) error { + dsn := fmt.Sprintf( + "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", + cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName, cfg.SSLMode, + ) + + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + return fmt.Errorf("failed to connect: %w", err) + } + + sqlDB, err := db.DB() + if err != nil { + return fmt.Errorf("failed to get db instance: %w", err) + } + defer sqlDB.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := sqlDB.PingContext(ctx); err != nil { + return fmt.Errorf("ping failed: %w", err) + } + + return nil +} + +// TestRedisConnection tests the Redis connection +func TestRedisConnection(cfg *RedisConfig) error { + rdb := redis.NewClient(&redis.Options{ + Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port), + Password: cfg.Password, + DB: cfg.DB, + }) + defer rdb.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := rdb.Ping(ctx).Err(); err != nil { + return fmt.Errorf("ping failed: %w", err) + } + + return nil +} + +// Install performs the installation with the given configuration +func Install(cfg *SetupConfig) error { + // Security check: prevent re-installation if already installed + if !NeedsSetup() { + return fmt.Errorf("system is already installed, re-installation is not allowed") + } + + // Generate JWT secret if not provided + if cfg.JWT.Secret == "" { + cfg.JWT.Secret = generateSecret(32) + } + + // Test connections + if err := TestDatabaseConnection(&cfg.Database); err != nil { + return fmt.Errorf("database connection failed: %w", err) + } + + if err := TestRedisConnection(&cfg.Redis); err != nil { + return fmt.Errorf("redis connection failed: %w", err) + } + + // Initialize database + if err := initializeDatabase(cfg); err != nil { + return fmt.Errorf("database initialization failed: %w", err) + } + + // Create admin user + if err := createAdminUser(cfg); err != nil { + return fmt.Errorf("admin user creation failed: %w", err) + } + + // Write config file + if err := writeConfigFile(cfg); err != nil { + return fmt.Errorf("config file creation failed: %w", err) + } + + // Create installation lock file to prevent re-setup attacks + if err := createInstallLock(); err != nil { + return fmt.Errorf("failed to create install lock: %w", err) + } + + return nil +} + +// createInstallLock creates a lock file to prevent re-installation attacks +func createInstallLock() error { + lockFile := ".installed" + content := fmt.Sprintf("installed_at=%s\n", time.Now().UTC().Format(time.RFC3339)) + return os.WriteFile(lockFile, []byte(content), 0400) // Read-only for owner +} + +func initializeDatabase(cfg *SetupConfig) error { + dsn := fmt.Sprintf( + "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", + cfg.Database.Host, cfg.Database.Port, cfg.Database.User, + cfg.Database.Password, cfg.Database.DBName, cfg.Database.SSLMode, + ) + + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + return err + } + + sqlDB, err := db.DB() + if err != nil { + return err + } + defer sqlDB.Close() + + // Run auto-migration for all models + return db.AutoMigrate( + &User{}, + &Group{}, + &APIKey{}, + &Account{}, + &Proxy{}, + &RedeemCode{}, + &UsageLog{}, + &UserSubscription{}, + &Setting{}, + ) +} + +func createAdminUser(cfg *SetupConfig) error { + dsn := fmt.Sprintf( + "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", + cfg.Database.Host, cfg.Database.Port, cfg.Database.User, + cfg.Database.Password, cfg.Database.DBName, cfg.Database.SSLMode, + ) + + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + return err + } + + sqlDB, err := db.DB() + if err != nil { + return err + } + defer sqlDB.Close() + + // Check if admin already exists + var count int64 + db.Model(&User{}).Where("role = ?", "admin").Count(&count) + if count > 0 { + return nil // Admin already exists + } + + // Hash password + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(cfg.Admin.Password), bcrypt.DefaultCost) + if err != nil { + return err + } + + // Create admin user + admin := &User{ + Email: cfg.Admin.Email, + PasswordHash: string(hashedPassword), + Role: "admin", + Status: "active", + Balance: 0, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + return db.Create(admin).Error +} + +func writeConfigFile(cfg *SetupConfig) error { + // Ensure timezone has a default value + tz := cfg.Timezone + if tz == "" { + tz = "Asia/Shanghai" + } + + // Prepare config for YAML (exclude sensitive data and admin config) + yamlConfig := struct { + Server ServerConfig `yaml:"server"` + Database DatabaseConfig `yaml:"database"` + Redis RedisConfig `yaml:"redis"` + JWT struct { + Secret string `yaml:"secret"` + ExpireHour int `yaml:"expire_hour"` + } `yaml:"jwt"` + Default struct { + GroupID uint `yaml:"group_id"` + } `yaml:"default"` + RateLimit struct { + RequestsPerMinute int `yaml:"requests_per_minute"` + BurstSize int `yaml:"burst_size"` + } `yaml:"rate_limit"` + Timezone string `yaml:"timezone"` + }{ + Server: cfg.Server, + Database: cfg.Database, + Redis: cfg.Redis, + JWT: struct { + Secret string `yaml:"secret"` + ExpireHour int `yaml:"expire_hour"` + }{ + Secret: cfg.JWT.Secret, + ExpireHour: cfg.JWT.ExpireHour, + }, + Default: struct { + GroupID uint `yaml:"group_id"` + }{ + GroupID: 1, + }, + RateLimit: struct { + RequestsPerMinute int `yaml:"requests_per_minute"` + BurstSize int `yaml:"burst_size"` + }{ + RequestsPerMinute: 60, + BurstSize: 10, + }, + Timezone: tz, + } + + data, err := yaml.Marshal(&yamlConfig) + if err != nil { + return err + } + + return os.WriteFile(ConfigFile, data, 0600) +} + +func generateSecret(length int) string { + bytes := make([]byte, length) + rand.Read(bytes) + return hex.EncodeToString(bytes) +} + +// Minimal model definitions for migration (to avoid circular import) +type User struct { + ID uint `gorm:"primaryKey"` + Email string `gorm:"uniqueIndex;not null"` + PasswordHash string `gorm:"not null"` + Role string `gorm:"default:user"` + Status string `gorm:"default:active"` + Balance float64 `gorm:"default:0"` + CreatedAt time.Time + UpdatedAt time.Time +} + +type Group struct { + ID uint `gorm:"primaryKey"` + Name string `gorm:"uniqueIndex;not null"` + Description string `gorm:"type:text"` + RateMultiplier float64 `gorm:"default:1.0"` + IsExclusive bool `gorm:"default:false"` + Priority int `gorm:"default:0"` + Status string `gorm:"default:active"` + CreatedAt time.Time + UpdatedAt time.Time +} + +type APIKey struct { + ID uint `gorm:"primaryKey"` + UserID uint `gorm:"index;not null"` + Key string `gorm:"uniqueIndex;not null"` + Name string + GroupID *uint + Status string `gorm:"default:active"` + CreatedAt time.Time + UpdatedAt time.Time +} + +type Account struct { + ID uint `gorm:"primaryKey"` + Platform string `gorm:"not null"` + Type string `gorm:"not null"` + Credentials string `gorm:"type:text"` + Status string `gorm:"default:active"` + Priority int `gorm:"default:0"` + ProxyID *uint + CreatedAt time.Time + UpdatedAt time.Time +} + +type Proxy struct { + ID uint `gorm:"primaryKey"` + Name string `gorm:"not null"` + Protocol string `gorm:"not null"` + Host string `gorm:"not null"` + Port int `gorm:"not null"` + Username string + Password string + Status string `gorm:"default:active"` + CreatedAt time.Time + UpdatedAt time.Time +} + +type RedeemCode struct { + ID uint `gorm:"primaryKey"` + Code string `gorm:"uniqueIndex;not null"` + Value float64 `gorm:"not null"` + Status string `gorm:"default:unused"` + UsedBy *uint + UsedAt *time.Time + ExpiresAt *time.Time + CreatedAt time.Time +} + +type UsageLog struct { + ID uint `gorm:"primaryKey"` + UserID uint `gorm:"index"` + APIKeyID uint `gorm:"index"` + AccountID *uint `gorm:"index"` + Model string `gorm:"index"` + InputTokens int + OutputTokens int + Cost float64 + CreatedAt time.Time +} + +type UserSubscription struct { + ID uint `gorm:"primaryKey"` + UserID uint `gorm:"index;not null"` + GroupID uint `gorm:"index;not null"` + Quota int64 + Used int64 `gorm:"default:0"` + Status string + ExpiresAt *time.Time + CreatedAt time.Time + UpdatedAt time.Time +} + +type Setting struct { + ID uint `gorm:"primaryKey"` + Key string `gorm:"uniqueIndex;not null"` + Value string `gorm:"type:text"` + CreatedAt time.Time + UpdatedAt time.Time +} + +func (User) TableName() string { return "users" } +func (Group) TableName() string { return "groups" } +func (APIKey) TableName() string { return "api_keys" } +func (Account) TableName() string { return "accounts" } +func (Proxy) TableName() string { return "proxies" } +func (RedeemCode) TableName() string { return "redeem_codes" } +func (UsageLog) TableName() string { return "usage_logs" } +func (UserSubscription) TableName() string { return "user_subscriptions" } +func (Setting) TableName() string { return "settings" } + +// ============================================================================= +// Auto Setup for Docker Deployment +// ============================================================================= + +// AutoSetupEnabled checks if auto setup is enabled via environment variable +func AutoSetupEnabled() bool { + val := os.Getenv("AUTO_SETUP") + return val == "true" || val == "1" || val == "yes" +} + +// getEnvOrDefault gets environment variable or returns default value +func getEnvOrDefault(key, defaultValue string) string { + if val := os.Getenv(key); val != "" { + return val + } + return defaultValue +} + +// getEnvIntOrDefault gets environment variable as int or returns default value +func getEnvIntOrDefault(key string, defaultValue int) int { + if val := os.Getenv(key); val != "" { + if i, err := strconv.Atoi(val); err == nil { + return i + } + } + return defaultValue +} + +// AutoSetupFromEnv performs automatic setup using environment variables +// This is designed for Docker deployment where all config is passed via env vars +func AutoSetupFromEnv() error { + log.Println("Auto setup enabled, configuring from environment variables...") + + // Get timezone from TZ or TIMEZONE env var (TZ is standard for Docker) + tz := getEnvOrDefault("TZ", "") + if tz == "" { + tz = getEnvOrDefault("TIMEZONE", "Asia/Shanghai") + } + + // Build config from environment variables + cfg := &SetupConfig{ + Database: DatabaseConfig{ + Host: getEnvOrDefault("DATABASE_HOST", "localhost"), + Port: getEnvIntOrDefault("DATABASE_PORT", 5432), + User: getEnvOrDefault("DATABASE_USER", "postgres"), + Password: getEnvOrDefault("DATABASE_PASSWORD", ""), + DBName: getEnvOrDefault("DATABASE_DBNAME", "sub2api"), + SSLMode: getEnvOrDefault("DATABASE_SSLMODE", "disable"), + }, + Redis: RedisConfig{ + Host: getEnvOrDefault("REDIS_HOST", "localhost"), + Port: getEnvIntOrDefault("REDIS_PORT", 6379), + Password: getEnvOrDefault("REDIS_PASSWORD", ""), + DB: getEnvIntOrDefault("REDIS_DB", 0), + }, + Admin: AdminConfig{ + Email: getEnvOrDefault("ADMIN_EMAIL", "admin@sub2api.local"), + Password: getEnvOrDefault("ADMIN_PASSWORD", ""), + }, + Server: ServerConfig{ + Host: getEnvOrDefault("SERVER_HOST", "0.0.0.0"), + Port: getEnvIntOrDefault("SERVER_PORT", 8080), + Mode: getEnvOrDefault("SERVER_MODE", "release"), + }, + JWT: JWTConfig{ + Secret: getEnvOrDefault("JWT_SECRET", ""), + ExpireHour: getEnvIntOrDefault("JWT_EXPIRE_HOUR", 24), + }, + Timezone: tz, + } + + // Generate JWT secret if not provided + if cfg.JWT.Secret == "" { + cfg.JWT.Secret = generateSecret(32) + log.Println("Generated JWT secret automatically") + } + + // Generate admin password if not provided + if cfg.Admin.Password == "" { + cfg.Admin.Password = generateSecret(16) + log.Printf("Generated admin password: %s", cfg.Admin.Password) + log.Println("IMPORTANT: Save this password! It will not be shown again.") + } + + // Test database connection + log.Println("Testing database connection...") + if err := TestDatabaseConnection(&cfg.Database); err != nil { + return fmt.Errorf("database connection failed: %w", err) + } + log.Println("Database connection successful") + + // Test Redis connection + log.Println("Testing Redis connection...") + if err := TestRedisConnection(&cfg.Redis); err != nil { + return fmt.Errorf("redis connection failed: %w", err) + } + log.Println("Redis connection successful") + + // Initialize database + log.Println("Initializing database...") + if err := initializeDatabase(cfg); err != nil { + return fmt.Errorf("database initialization failed: %w", err) + } + log.Println("Database initialized successfully") + + // Create admin user + log.Println("Creating admin user...") + if err := createAdminUser(cfg); err != nil { + return fmt.Errorf("admin user creation failed: %w", err) + } + log.Printf("Admin user created: %s", cfg.Admin.Email) + + // Write config file + log.Println("Writing configuration file...") + if err := writeConfigFile(cfg); err != nil { + return fmt.Errorf("config file creation failed: %w", err) + } + log.Println("Configuration file created") + + // Create installation lock file + if err := createInstallLock(); err != nil { + return fmt.Errorf("failed to create install lock: %w", err) + } + log.Println("Installation lock created") + + log.Println("Auto setup completed successfully!") + return nil +} diff --git a/backend/internal/web/embed.go b/backend/internal/web/embed.go new file mode 100644 index 00000000..93cfe763 --- /dev/null +++ b/backend/internal/web/embed.go @@ -0,0 +1,79 @@ +package web + +import ( + "embed" + "io" + "io/fs" + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +//go:embed dist/* +var frontendFS embed.FS + +// ServeEmbeddedFrontend returns a Gin handler that serves embedded frontend assets +// and handles SPA routing by falling back to index.html for non-API routes. +func ServeEmbeddedFrontend() gin.HandlerFunc { + distFS, err := fs.Sub(frontendFS, "dist") + if err != nil { + panic("failed to get dist subdirectory: " + err.Error()) + } + fileServer := http.FileServer(http.FS(distFS)) + + return func(c *gin.Context) { + path := c.Request.URL.Path + + // Skip API and gateway routes + if strings.HasPrefix(path, "/api/") || + strings.HasPrefix(path, "/v1/") || + strings.HasPrefix(path, "/setup/") || + path == "/health" { + c.Next() + return + } + + // Try to serve static file + cleanPath := strings.TrimPrefix(path, "/") + if cleanPath == "" { + cleanPath = "index.html" + } + + if file, err := distFS.Open(cleanPath); err == nil { + file.Close() + fileServer.ServeHTTP(c.Writer, c.Request) + c.Abort() + return + } + + // SPA fallback: serve index.html for all other routes + serveIndexHTML(c, distFS) + } +} + +func serveIndexHTML(c *gin.Context, fsys fs.FS) { + file, err := fsys.Open("index.html") + if err != nil { + c.String(http.StatusNotFound, "Frontend not found") + c.Abort() + return + } + defer file.Close() + + content, err := io.ReadAll(file) + if err != nil { + c.String(http.StatusInternalServerError, "Failed to read index.html") + c.Abort() + return + } + + c.Data(http.StatusOK, "text/html; charset=utf-8", content) + c.Abort() +} + +// HasEmbeddedFrontend checks if frontend assets are embedded +func HasEmbeddedFrontend() bool { + _, err := frontendFS.ReadFile("dist/index.html") + return err == nil +} diff --git a/backend/migrations/001_init.sql b/backend/migrations/001_init.sql new file mode 100644 index 00000000..fea194c0 --- /dev/null +++ b/backend/migrations/001_init.sql @@ -0,0 +1,183 @@ +-- Sub2API 初始化数据库迁移脚本 +-- PostgreSQL 15+ + +-- 1. proxies 代理IP表(无外键依赖) +CREATE TABLE IF NOT EXISTS proxies ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + protocol VARCHAR(20) NOT NULL, -- http/https/socks5 + host VARCHAR(255) NOT NULL, + port INT NOT NULL, + username VARCHAR(100), + password VARCHAR(100), + status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/disabled + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_proxies_status ON proxies(status); +CREATE INDEX IF NOT EXISTS idx_proxies_deleted_at ON proxies(deleted_at); + +-- 2. groups 分组表(无外键依赖) +CREATE TABLE IF NOT EXISTS groups ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL UNIQUE, + description TEXT, + rate_multiplier DECIMAL(10, 4) NOT NULL DEFAULT 1.0, -- 费率倍率 + is_exclusive BOOLEAN NOT NULL DEFAULT FALSE, -- 是否专属分组 + status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/disabled + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_groups_name ON groups(name); +CREATE INDEX IF NOT EXISTS idx_groups_status ON groups(status); +CREATE INDEX IF NOT EXISTS idx_groups_is_exclusive ON groups(is_exclusive); +CREATE INDEX IF NOT EXISTS idx_groups_deleted_at ON groups(deleted_at); + +-- 3. users 用户表(无外键依赖) +CREATE TABLE IF NOT EXISTS users ( + id BIGSERIAL PRIMARY KEY, + email VARCHAR(255) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + role VARCHAR(20) NOT NULL DEFAULT 'user', -- admin/user + balance DECIMAL(20, 8) NOT NULL DEFAULT 0, -- 余额(可为负数) + concurrency INT NOT NULL DEFAULT 5, -- 并发数限制 + status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/disabled + allowed_groups BIGINT[] DEFAULT NULL, -- 允许绑定的分组ID列表 + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); +CREATE INDEX IF NOT EXISTS idx_users_status ON users(status); +CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users(deleted_at); + +-- 4. accounts 上游账号表(依赖proxies) +CREATE TABLE IF NOT EXISTS accounts ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + platform VARCHAR(50) NOT NULL, -- anthropic/openai/gemini + type VARCHAR(20) NOT NULL, -- oauth/apikey + credentials JSONB NOT NULL DEFAULT '{}', -- 凭证信息(加密存储) + extra JSONB NOT NULL DEFAULT '{}', -- 扩展信息 + proxy_id BIGINT REFERENCES proxies(id) ON DELETE SET NULL, + concurrency INT NOT NULL DEFAULT 3, -- 账号并发限制 + priority INT NOT NULL DEFAULT 50, -- 调度优先级(1-100,越小越高) + status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/disabled/error + error_message TEXT, + last_used_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_accounts_platform ON accounts(platform); +CREATE INDEX IF NOT EXISTS idx_accounts_type ON accounts(type); +CREATE INDEX IF NOT EXISTS idx_accounts_status ON accounts(status); +CREATE INDEX IF NOT EXISTS idx_accounts_proxy_id ON accounts(proxy_id); +CREATE INDEX IF NOT EXISTS idx_accounts_priority ON accounts(priority); +CREATE INDEX IF NOT EXISTS idx_accounts_last_used_at ON accounts(last_used_at); +CREATE INDEX IF NOT EXISTS idx_accounts_deleted_at ON accounts(deleted_at); + +-- 5. api_keys API密钥表(依赖users, groups) +CREATE TABLE IF NOT EXISTS api_keys ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + key VARCHAR(64) NOT NULL UNIQUE, -- sk-xxx格式 + name VARCHAR(100) NOT NULL, + group_id BIGINT REFERENCES groups(id) ON DELETE SET NULL, + status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/disabled + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_api_keys_key ON api_keys(key); +CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id); +CREATE INDEX IF NOT EXISTS idx_api_keys_group_id ON api_keys(group_id); +CREATE INDEX IF NOT EXISTS idx_api_keys_status ON api_keys(status); +CREATE INDEX IF NOT EXISTS idx_api_keys_deleted_at ON api_keys(deleted_at); + +-- 6. account_groups 账号-分组关联表(依赖accounts, groups) +CREATE TABLE IF NOT EXISTS account_groups ( + account_id BIGINT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + group_id BIGINT NOT NULL REFERENCES groups(id) ON DELETE CASCADE, + priority INT NOT NULL DEFAULT 50, -- 分组内优先级 + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (account_id, group_id) +); + +CREATE INDEX IF NOT EXISTS idx_account_groups_group_id ON account_groups(group_id); +CREATE INDEX IF NOT EXISTS idx_account_groups_priority ON account_groups(priority); + +-- 7. redeem_codes 卡密表(依赖users) +CREATE TABLE IF NOT EXISTS redeem_codes ( + id BIGSERIAL PRIMARY KEY, + code VARCHAR(32) NOT NULL UNIQUE, -- 兑换码 + type VARCHAR(20) NOT NULL DEFAULT 'balance', -- balance + value DECIMAL(20, 8) NOT NULL, -- 面值(USD) + status VARCHAR(20) NOT NULL DEFAULT 'unused', -- unused/used + used_by BIGINT REFERENCES users(id) ON DELETE SET NULL, + used_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_redeem_codes_code ON redeem_codes(code); +CREATE INDEX IF NOT EXISTS idx_redeem_codes_status ON redeem_codes(status); +CREATE INDEX IF NOT EXISTS idx_redeem_codes_used_by ON redeem_codes(used_by); + +-- 8. usage_logs 使用记录表(依赖users, api_keys, accounts) +CREATE TABLE IF NOT EXISTS usage_logs ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + api_key_id BIGINT NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE, + account_id BIGINT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + request_id VARCHAR(64), + model VARCHAR(100) NOT NULL, + + -- Token使用量(4类) + input_tokens INT NOT NULL DEFAULT 0, + output_tokens INT NOT NULL DEFAULT 0, + cache_creation_tokens INT NOT NULL DEFAULT 0, + cache_read_tokens INT NOT NULL DEFAULT 0, + + -- 详细的缓存创建分类 + cache_creation_5m_tokens INT NOT NULL DEFAULT 0, + cache_creation_1h_tokens INT NOT NULL DEFAULT 0, + + -- 费用(USD) + input_cost DECIMAL(20, 10) NOT NULL DEFAULT 0, + output_cost DECIMAL(20, 10) NOT NULL DEFAULT 0, + cache_creation_cost DECIMAL(20, 10) NOT NULL DEFAULT 0, + cache_read_cost DECIMAL(20, 10) NOT NULL DEFAULT 0, + total_cost DECIMAL(20, 10) NOT NULL DEFAULT 0, -- 原始总费用 + actual_cost DECIMAL(20, 10) NOT NULL DEFAULT 0, -- 实际扣除费用 + + -- 元数据 + stream BOOLEAN NOT NULL DEFAULT FALSE, + duration_ms INT, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_usage_logs_user_id ON usage_logs(user_id); +CREATE INDEX IF NOT EXISTS idx_usage_logs_api_key_id ON usage_logs(api_key_id); +CREATE INDEX IF NOT EXISTS idx_usage_logs_account_id ON usage_logs(account_id); +CREATE INDEX IF NOT EXISTS idx_usage_logs_model ON usage_logs(model); +CREATE INDEX IF NOT EXISTS idx_usage_logs_created_at ON usage_logs(created_at); +CREATE INDEX IF NOT EXISTS idx_usage_logs_user_created ON usage_logs(user_id, created_at); + +-- 插入默认管理员用户 +-- 密码: admin123 (bcrypt hash) +INSERT INTO users (email, password_hash, role, balance, concurrency, status) +VALUES ('admin@sub2api.com', '$2a$10$N9qo8uLOickgx2ZMRZoMye.IjJbDdJeCo0U2bBPJj9lS/5LqD.C.C', 'admin', 0, 10, 'active') +ON CONFLICT (email) DO NOTHING; + +-- 插入默认分组 +INSERT INTO groups (name, description, rate_multiplier, is_exclusive, status) +VALUES ('default', '默认分组', 1.0, false, 'active') +ON CONFLICT (name) DO NOTHING; diff --git a/backend/migrations/002_account_type_migration.sql b/backend/migrations/002_account_type_migration.sql new file mode 100644 index 00000000..b1c955ef --- /dev/null +++ b/backend/migrations/002_account_type_migration.sql @@ -0,0 +1,33 @@ +-- Sub2API 账号类型迁移脚本 +-- 将 'official' 类型账号迁移为 'oauth' 或 'setup-token' +-- 根据 credentials->>'scope' 字段判断: +-- - 包含 'user:profile' 的是 'oauth' 类型 +-- - 只有 'user:inference' 的是 'setup-token' 类型 + +-- 1. 将包含 profile scope 的 official 账号迁移为 oauth +UPDATE accounts +SET type = 'oauth', + updated_at = NOW() +WHERE type = 'official' + AND credentials->>'scope' LIKE '%user:profile%'; + +-- 2. 将只有 inference scope 的 official 账号迁移为 setup-token +UPDATE accounts +SET type = 'setup-token', + updated_at = NOW() +WHERE type = 'official' + AND ( + credentials->>'scope' = 'user:inference' + OR credentials->>'scope' NOT LIKE '%user:profile%' + ); + +-- 3. 处理没有 scope 字段的旧账号(默认为 oauth) +UPDATE accounts +SET type = 'oauth', + updated_at = NOW() +WHERE type = 'official' + AND (credentials->>'scope' IS NULL OR credentials->>'scope' = ''); + +-- 4. 验证迁移结果(查询是否还有 official 类型账号) +-- SELECT COUNT(*) FROM accounts WHERE type = 'official'; +-- 如果结果为 0,说明迁移成功 diff --git a/backend/migrations/003_subscription.sql b/backend/migrations/003_subscription.sql new file mode 100644 index 00000000..d9c54a32 --- /dev/null +++ b/backend/migrations/003_subscription.sql @@ -0,0 +1,65 @@ +-- Sub2API 订阅功能迁移脚本 +-- 添加订阅分组和用户订阅功能 + +-- 1. 扩展 groups 表添加订阅相关字段 +ALTER TABLE groups ADD COLUMN IF NOT EXISTS platform VARCHAR(50) NOT NULL DEFAULT 'anthropic'; +ALTER TABLE groups ADD COLUMN IF NOT EXISTS subscription_type VARCHAR(20) NOT NULL DEFAULT 'standard'; +ALTER TABLE groups ADD COLUMN IF NOT EXISTS daily_limit_usd DECIMAL(20, 8) DEFAULT NULL; +ALTER TABLE groups ADD COLUMN IF NOT EXISTS weekly_limit_usd DECIMAL(20, 8) DEFAULT NULL; +ALTER TABLE groups ADD COLUMN IF NOT EXISTS monthly_limit_usd DECIMAL(20, 8) DEFAULT NULL; +ALTER TABLE groups ADD COLUMN IF NOT EXISTS default_validity_days INT NOT NULL DEFAULT 30; + +-- 添加索引 +CREATE INDEX IF NOT EXISTS idx_groups_platform ON groups(platform); +CREATE INDEX IF NOT EXISTS idx_groups_subscription_type ON groups(subscription_type); + +-- 2. 创建 user_subscriptions 用户订阅表 +CREATE TABLE IF NOT EXISTS user_subscriptions ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + group_id BIGINT NOT NULL REFERENCES groups(id) ON DELETE CASCADE, + + -- 订阅有效期 + starts_at TIMESTAMPTZ NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'active', -- active/expired/suspended + + -- 滑动窗口起始时间(NULL=未激活) + daily_window_start TIMESTAMPTZ, + weekly_window_start TIMESTAMPTZ, + monthly_window_start TIMESTAMPTZ, + + -- 当前窗口已用额度(USD,基于 total_cost 计算) + daily_usage_usd DECIMAL(20, 10) NOT NULL DEFAULT 0, + weekly_usage_usd DECIMAL(20, 10) NOT NULL DEFAULT 0, + monthly_usage_usd DECIMAL(20, 10) NOT NULL DEFAULT 0, + + -- 管理员分配信息 + assigned_by BIGINT REFERENCES users(id) ON DELETE SET NULL, + assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + notes TEXT, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + -- 唯一约束:每个用户对每个分组只能有一个订阅 + UNIQUE(user_id, group_id) +); + +-- user_subscriptions 索引 +CREATE INDEX IF NOT EXISTS idx_user_subscriptions_user_id ON user_subscriptions(user_id); +CREATE INDEX IF NOT EXISTS idx_user_subscriptions_group_id ON user_subscriptions(group_id); +CREATE INDEX IF NOT EXISTS idx_user_subscriptions_status ON user_subscriptions(status); +CREATE INDEX IF NOT EXISTS idx_user_subscriptions_expires_at ON user_subscriptions(expires_at); +CREATE INDEX IF NOT EXISTS idx_user_subscriptions_assigned_by ON user_subscriptions(assigned_by); + +-- 3. 扩展 usage_logs 表添加分组和订阅关联 +ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS group_id BIGINT REFERENCES groups(id) ON DELETE SET NULL; +ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS subscription_id BIGINT REFERENCES user_subscriptions(id) ON DELETE SET NULL; +ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS rate_multiplier DECIMAL(10, 4) NOT NULL DEFAULT 1; +ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS first_token_ms INT; + +-- usage_logs 新索引 +CREATE INDEX IF NOT EXISTS idx_usage_logs_group_id ON usage_logs(group_id); +CREATE INDEX IF NOT EXISTS idx_usage_logs_subscription_id ON usage_logs(subscription_id); +CREATE INDEX IF NOT EXISTS idx_usage_logs_sub_created ON usage_logs(subscription_id, created_at); diff --git a/backend/resources/model-pricing/README.md b/backend/resources/model-pricing/README.md new file mode 100644 index 00000000..d755de73 --- /dev/null +++ b/backend/resources/model-pricing/README.md @@ -0,0 +1,37 @@ +# Model Pricing Data + +This directory contains a local copy of the mirrored model pricing data as a fallback mechanism. + +## Source +The original file is maintained by the LiteLLM project and mirrored into the `price-mirror` branch of this repository via GitHub Actions: +- Mirror branch (configurable via `PRICE_MIRROR_REPO`): https://raw.githubusercontent.com//price-mirror/model_prices_and_context_window.json +- Upstream source: https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json + +## Purpose +This local copy serves as a fallback when the remote file cannot be downloaded due to: +- Network restrictions +- Firewall rules +- DNS resolution issues +- GitHub being blocked in certain regions +- Docker container network limitations + +## Update Process +The pricingService will: +1. First attempt to download the latest version from GitHub +2. If download fails, use this local copy as fallback +3. Log a warning when using the fallback file + +## Manual Update +To manually update this file with the latest pricing data (if automation is unavailable): +```bash +curl -s https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json -o model_prices_and_context_window.json +``` + +## File Format +The file contains JSON data with model pricing information including: +- Model names and identifiers +- Input/output token costs +- Context window sizes +- Model capabilities + +Last updated: 2025-08-10 diff --git a/backend/resources/model-pricing/model_prices_and_context_window.json b/backend/resources/model-pricing/model_prices_and_context_window.json new file mode 100644 index 00000000..ad2861df --- /dev/null +++ b/backend/resources/model-pricing/model_prices_and_context_window.json @@ -0,0 +1,31356 @@ +{ + "sample_spec": { + "code_interpreter_cost_per_session": 0.0, + "computer_use_input_cost_per_1k_tokens": 0.0, + "computer_use_output_cost_per_1k_tokens": 0.0, + "deprecation_date": "date when the model becomes deprecated in the format YYYY-MM-DD", + "file_search_cost_per_1k_calls": 0.0, + "file_search_cost_per_gb_per_day": 0.0, + "input_cost_per_audio_token": 0.0, + "input_cost_per_token": 0.0, + "litellm_provider": "one of https://docs.litellm.ai/docs/providers", + "max_input_tokens": "max input tokens, if the provider specifies it. if not default to max_tokens", + "max_output_tokens": "max output tokens, if the provider specifies it. if not default to max_tokens", + "max_tokens": "LEGACY parameter. set to max_output_tokens if provider specifies it. IF not set to max_input_tokens, if provider specifies it.", + "mode": "one of: chat, embedding, completion, image_generation, audio_transcription, audio_speech, image_generation, moderation, rerank, search", + "output_cost_per_reasoning_token": 0.0, + "output_cost_per_token": 0.0, + "search_context_cost_per_query": { + "search_context_size_high": 0.0, + "search_context_size_low": 0.0, + "search_context_size_medium": 0.0 + }, + "supported_regions": [ + "global", + "us-west-2", + "eu-west-1", + "ap-southeast-1", + "ap-northeast-1" + ], + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_vision": true, + "supports_web_search": true, + "vector_store_cost_per_gb_per_day": 0.0 + }, + "1024-x-1024/50-steps/bedrock/amazon.nova-canvas-v1:0": { + "litellm_provider": "bedrock", + "max_input_tokens": 2600, + "mode": "image_generation", + "output_cost_per_image": 0.06 + }, + "1024-x-1024/50-steps/stability.stable-diffusion-xl-v1": { + "litellm_provider": "bedrock", + "max_input_tokens": 77, + "max_tokens": 77, + "mode": "image_generation", + "output_cost_per_image": 0.04 + }, + "1024-x-1024/dall-e-2": { + "input_cost_per_pixel": 1.9e-08, + "litellm_provider": "openai", + "mode": "image_generation", + "output_cost_per_pixel": 0.0 + }, + "1024-x-1024/max-steps/stability.stable-diffusion-xl-v1": { + "litellm_provider": "bedrock", + "max_input_tokens": 77, + "max_tokens": 77, + "mode": "image_generation", + "output_cost_per_image": 0.08 + }, + "256-x-256/dall-e-2": { + "input_cost_per_pixel": 2.4414e-07, + "litellm_provider": "openai", + "mode": "image_generation", + "output_cost_per_pixel": 0.0 + }, + "512-x-512/50-steps/stability.stable-diffusion-xl-v0": { + "litellm_provider": "bedrock", + "max_input_tokens": 77, + "max_tokens": 77, + "mode": "image_generation", + "output_cost_per_image": 0.018 + }, + "512-x-512/dall-e-2": { + "input_cost_per_pixel": 6.86e-08, + "litellm_provider": "openai", + "mode": "image_generation", + "output_cost_per_pixel": 0.0 + }, + "512-x-512/max-steps/stability.stable-diffusion-xl-v0": { + "litellm_provider": "bedrock", + "max_input_tokens": 77, + "max_tokens": 77, + "mode": "image_generation", + "output_cost_per_image": 0.036 + }, + "ai21.j2-mid-v1": { + "input_cost_per_token": 1.25e-05, + "litellm_provider": "bedrock", + "max_input_tokens": 8191, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 1.25e-05 + }, + "ai21.j2-ultra-v1": { + "input_cost_per_token": 1.88e-05, + "litellm_provider": "bedrock", + "max_input_tokens": 8191, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 1.88e-05 + }, + "ai21.jamba-1-5-large-v1:0": { + "input_cost_per_token": 2e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 8e-06 + }, + "ai21.jamba-1-5-mini-v1:0": { + "input_cost_per_token": 2e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 4e-07 + }, + "ai21.jamba-instruct-v1:0": { + "input_cost_per_token": 5e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 70000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 7e-07, + "supports_system_messages": true + }, + "aiml/dall-e-2": { + "litellm_provider": "aiml", + "metadata": { + "notes": "DALL-E 2 via AI/ML API - Reliable text-to-image generation" + }, + "mode": "image_generation", + "output_cost_per_image": 0.021, + "source": "https://docs.aimlapi.com/", + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "aiml/dall-e-3": { + "litellm_provider": "aiml", + "metadata": { + "notes": "DALL-E 3 via AI/ML API - High-quality text-to-image generation" + }, + "mode": "image_generation", + "output_cost_per_image": 0.042, + "source": "https://docs.aimlapi.com/", + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "aiml/flux-pro": { + "litellm_provider": "aiml", + "metadata": { + "notes": "Flux Dev - Development version optimized for experimentation" + }, + "mode": "image_generation", + "output_cost_per_image": 0.053, + "source": "https://docs.aimlapi.com/", + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "aiml/flux-pro/v1.1": { + "litellm_provider": "aiml", + "mode": "image_generation", + "output_cost_per_image": 0.042, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "aiml/flux-pro/v1.1-ultra": { + "litellm_provider": "aiml", + "mode": "image_generation", + "output_cost_per_image": 0.063, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "aiml/flux-realism": { + "litellm_provider": "aiml", + "metadata": { + "notes": "Flux Pro - Professional-grade image generation model" + }, + "mode": "image_generation", + "output_cost_per_image": 0.037, + "source": "https://docs.aimlapi.com/", + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "aiml/flux/dev": { + "litellm_provider": "aiml", + "metadata": { + "notes": "Flux Dev - Development version optimized for experimentation" + }, + "mode": "image_generation", + "output_cost_per_image": 0.026, + "source": "https://docs.aimlapi.com/", + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "aiml/flux/kontext-max/text-to-image": { + "litellm_provider": "aiml", + "metadata": { + "notes": "Flux Pro v1.1 - Enhanced version with improved capabilities and 6x faster inference speed" + }, + "mode": "image_generation", + "output_cost_per_image": 0.084, + "source": "https://docs.aimlapi.com/", + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "aiml/flux/kontext-pro/text-to-image": { + "litellm_provider": "aiml", + "metadata": { + "notes": "Flux Pro v1.1 - Enhanced version with improved capabilities and 6x faster inference speed" + }, + "mode": "image_generation", + "output_cost_per_image": 0.042, + "source": "https://docs.aimlapi.com/", + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "aiml/flux/schnell": { + "litellm_provider": "aiml", + "metadata": { + "notes": "Flux Schnell - Fast generation model optimized for speed" + }, + "mode": "image_generation", + "output_cost_per_image": 0.003, + "source": "https://docs.aimlapi.com/", + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "amazon.nova-canvas-v1:0": { + "litellm_provider": "bedrock", + "max_input_tokens": 2600, + "mode": "image_generation", + "output_cost_per_image": 0.06 + }, + "us.writer.palmyra-x4-v1:0": { + "input_cost_per_token": 2.5e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supports_function_calling": true, + "supports_pdf_input": true + }, + "us.writer.palmyra-x5-v1:0": { + "input_cost_per_token": 6e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 1000000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 6e-06, + "supports_function_calling": true, + "supports_pdf_input": true + }, + "writer.palmyra-x4-v1:0": { + "input_cost_per_token": 2.5e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supports_function_calling": true, + "supports_pdf_input": true + }, + "writer.palmyra-x5-v1:0": { + "input_cost_per_token": 6e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 1000000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 6e-06, + "supports_function_calling": true, + "supports_pdf_input": true + }, + "amazon.nova-lite-v1:0": { + "input_cost_per_token": 6e-08, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 300000, + "max_output_tokens": 10000, + "max_tokens": 10000, + "mode": "chat", + "output_cost_per_token": 2.4e-07, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_vision": true + }, + "amazon.nova-2-lite-v1:0": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_token": 3e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.5e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_video_input": true, + "supports_vision": true + }, + "apac.amazon.nova-2-lite-v1:0": { + "cache_read_input_token_cost": 8.25e-08, + "input_cost_per_token": 3.3e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.75e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_video_input": true, + "supports_vision": true + }, + "eu.amazon.nova-2-lite-v1:0": { + "cache_read_input_token_cost": 8.25e-08, + "input_cost_per_token": 3.3e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.75e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_video_input": true, + "supports_vision": true + }, + "us.amazon.nova-2-lite-v1:0": { + "cache_read_input_token_cost": 8.25e-08, + "input_cost_per_token": 3.3e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.75e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_video_input": true, + "supports_vision": true + }, + + "amazon.nova-micro-v1:0": { + "input_cost_per_token": 3.5e-08, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 10000, + "max_tokens": 10000, + "mode": "chat", + "output_cost_per_token": 1.4e-07, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true + }, + "amazon.nova-pro-v1:0": { + "input_cost_per_token": 8e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 300000, + "max_output_tokens": 10000, + "max_tokens": 10000, + "mode": "chat", + "output_cost_per_token": 3.2e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_vision": true + }, + "amazon.rerank-v1:0": { + "input_cost_per_query": 0.001, + "input_cost_per_token": 0.0, + "litellm_provider": "bedrock", + "max_document_chunks_per_query": 100, + "max_input_tokens": 32000, + "max_output_tokens": 32000, + "max_query_tokens": 32000, + "max_tokens": 32000, + "max_tokens_per_document_chunk": 512, + "mode": "rerank", + "output_cost_per_token": 0.0 + }, + "amazon.titan-embed-image-v1": { + "input_cost_per_image": 6e-05, + "input_cost_per_token": 8e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 128, + "max_tokens": 128, + "metadata": { + "notes": "'supports_image_input' is a deprecated field. Use 'supports_embedding_image_input' instead." + }, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 1024, + "source": "https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/providers?model=amazon.titan-image-generator-v1", + "supports_embedding_image_input": true, + "supports_image_input": true + }, + "amazon.titan-embed-text-v1": { + "input_cost_per_token": 1e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 8192, + "max_tokens": 8192, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 1536 + }, + "amazon.titan-embed-text-v2:0": { + "input_cost_per_token": 2e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 8192, + "max_tokens": 8192, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 1024 + }, + "amazon.titan-image-generator-v1": { + "input_cost_per_image": 0.0, + "output_cost_per_image": 0.008, + "output_cost_per_image_premium_image": 0.01, + "output_cost_per_image_above_512_and_512_pixels": 0.01, + "output_cost_per_image_above_512_and_512_pixels_and_premium_image": 0.012, + "litellm_provider": "bedrock", + "mode": "image_generation" + }, + "amazon.titan-image-generator-v2": { + "input_cost_per_image": 0.0, + "output_cost_per_image": 0.008, + "output_cost_per_image_premium_image": 0.01, + "output_cost_per_image_above_1024_and_1024_pixels": 0.01, + "output_cost_per_image_above_1024_and_1024_pixels_and_premium_image": 0.012, + "litellm_provider": "bedrock", + "mode": "image_generation" + }, + "amazon.titan-image-generator-v2:0": { + "input_cost_per_image": 0.0, + "output_cost_per_image": 0.008, + "output_cost_per_image_premium_image": 0.01, + "output_cost_per_image_above_1024_and_1024_pixels": 0.01, + "output_cost_per_image_above_1024_and_1024_pixels_and_premium_image": 0.012, + "litellm_provider": "bedrock", + "mode": "image_generation" + }, + "twelvelabs.marengo-embed-2-7-v1:0": { + "input_cost_per_token": 7e-05, + "litellm_provider": "bedrock", + "max_input_tokens": 77, + "max_tokens": 77, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 1024, + "supports_embedding_image_input": true, + "supports_image_input": true + }, + "us.twelvelabs.marengo-embed-2-7-v1:0": { + "input_cost_per_token": 7e-05, + "input_cost_per_video_per_second": 0.0007, + "input_cost_per_audio_per_second": 0.00014, + "input_cost_per_image": 0.0001, + "litellm_provider": "bedrock", + "max_input_tokens": 77, + "max_tokens": 77, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 1024, + "supports_embedding_image_input": true, + "supports_image_input": true + }, + "eu.twelvelabs.marengo-embed-2-7-v1:0": { + "input_cost_per_token": 7e-05, + "input_cost_per_video_per_second": 0.0007, + "input_cost_per_audio_per_second": 0.00014, + "input_cost_per_image": 0.0001, + "litellm_provider": "bedrock", + "max_input_tokens": 77, + "max_tokens": 77, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 1024, + "supports_embedding_image_input": true, + "supports_image_input": true + }, + "twelvelabs.pegasus-1-2-v1:0": { + "input_cost_per_video_per_second": 0.00049, + "output_cost_per_token": 7.5e-06, + "litellm_provider": "bedrock", + "mode": "chat", + "supports_video_input": true + }, + "us.twelvelabs.pegasus-1-2-v1:0": { + "input_cost_per_video_per_second": 0.00049, + "output_cost_per_token": 7.5e-06, + "litellm_provider": "bedrock", + "mode": "chat", + "supports_video_input": true + }, + "eu.twelvelabs.pegasus-1-2-v1:0": { + "input_cost_per_video_per_second": 0.00049, + "output_cost_per_token": 7.5e-06, + "litellm_provider": "bedrock", + "mode": "chat", + "supports_video_input": true + }, + "amazon.titan-text-express-v1": { + "input_cost_per_token": 1.3e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 42000, + "max_output_tokens": 8000, + "max_tokens": 8000, + "mode": "chat", + "output_cost_per_token": 1.7e-06 + }, + "amazon.titan-text-lite-v1": { + "input_cost_per_token": 3e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 42000, + "max_output_tokens": 4000, + "max_tokens": 4000, + "mode": "chat", + "output_cost_per_token": 4e-07 + }, + "amazon.titan-text-premier-v1:0": { + "input_cost_per_token": 5e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 42000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 1.5e-06 + }, + "anthropic.claude-3-5-haiku-20241022-v1:0": { + "cache_creation_input_token_cost": 1e-06, + "cache_read_input_token_cost": 8e-08, + "input_cost_per_token": 8e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 4e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "anthropic.claude-haiku-4-5-20251001-v1:0": { + "cache_creation_input_token_cost": 1.25e-06, + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 1e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 5e-06, + "source": "https://aws.amazon.com/about-aws/whats-new/2025/10/claude-4-5-haiku-anthropic-amazon-bedrock", + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 346 + }, + "anthropic.claude-haiku-4-5@20251001": { + "cache_creation_input_token_cost": 1.25e-06, + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 1e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 5e-06, + "source": "https://aws.amazon.com/about-aws/whats-new/2025/10/claude-4-5-haiku-anthropic-amazon-bedrock", + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 346 + }, + "anthropic.claude-3-5-sonnet-20240620-v1:0": { + "input_cost_per_token": 3e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "anthropic.claude-3-5-sonnet-20241022-v2:0": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "anthropic.claude-3-7-sonnet-20240620-v1:0": { + "cache_creation_input_token_cost": 4.5e-06, + "cache_read_input_token_cost": 3.6e-07, + "input_cost_per_token": 3.6e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.8e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "anthropic.claude-3-7-sonnet-20250219-v1:0": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "anthropic.claude-3-haiku-20240307-v1:0": { + "input_cost_per_token": 2.5e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.25e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "anthropic.claude-3-opus-20240229-v1:0": { + "input_cost_per_token": 1.5e-05, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "anthropic.claude-3-sonnet-20240229-v1:0": { + "input_cost_per_token": 3e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "anthropic.claude-instant-v1": { + "input_cost_per_token": 8e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2.4e-06, + "supports_tool_choice": true + }, + "anthropic.claude-opus-4-1-20250805-v1:0": { + "cache_creation_input_token_cost": 1.875e-05, + "cache_read_input_token_cost": 1.5e-06, + "input_cost_per_token": 1.5e-05, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "anthropic.claude-opus-4-20250514-v1:0": { + "cache_creation_input_token_cost": 1.875e-05, + "cache_read_input_token_cost": 1.5e-06, + "input_cost_per_token": 1.5e-05, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "anthropic.claude-opus-4-5-20251101-v1:0": { + "cache_creation_input_token_cost": 6.25e-06, + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 5e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "anthropic.claude-sonnet-4-20250514-v1:0": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "anthropic.claude-sonnet-4-5-20250929-v1:0": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "anthropic.claude-v1": { + "input_cost_per_token": 8e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2.4e-05 + }, + "anthropic.claude-v2:1": { + "input_cost_per_token": 8e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2.4e-05, + "supports_tool_choice": true + }, + "anyscale/HuggingFaceH4/zephyr-7b-beta": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "anyscale", + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1.5e-07 + }, + "anyscale/codellama/CodeLlama-34b-Instruct-hf": { + "input_cost_per_token": 1e-06, + "litellm_provider": "anyscale", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1e-06 + }, + "anyscale/codellama/CodeLlama-70b-Instruct-hf": { + "input_cost_per_token": 1e-06, + "litellm_provider": "anyscale", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1e-06, + "source": "https://docs.anyscale.com/preview/endpoints/text-generation/supported-models/codellama-CodeLlama-70b-Instruct-hf" + }, + "anyscale/google/gemma-7b-it": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "anyscale", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.5e-07, + "source": "https://docs.anyscale.com/preview/endpoints/text-generation/supported-models/google-gemma-7b-it" + }, + "anyscale/meta-llama/Llama-2-13b-chat-hf": { + "input_cost_per_token": 2.5e-07, + "litellm_provider": "anyscale", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 2.5e-07 + }, + "anyscale/meta-llama/Llama-2-70b-chat-hf": { + "input_cost_per_token": 1e-06, + "litellm_provider": "anyscale", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1e-06 + }, + "anyscale/meta-llama/Llama-2-7b-chat-hf": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "anyscale", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-07 + }, + "anyscale/meta-llama/Meta-Llama-3-70B-Instruct": { + "input_cost_per_token": 1e-06, + "litellm_provider": "anyscale", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1e-06, + "source": "https://docs.anyscale.com/preview/endpoints/text-generation/supported-models/meta-llama-Meta-Llama-3-70B-Instruct" + }, + "anyscale/meta-llama/Meta-Llama-3-8B-Instruct": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "anyscale", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.5e-07, + "source": "https://docs.anyscale.com/preview/endpoints/text-generation/supported-models/meta-llama-Meta-Llama-3-8B-Instruct" + }, + "anyscale/mistralai/Mistral-7B-Instruct-v0.1": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "anyscale", + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1.5e-07, + "source": "https://docs.anyscale.com/preview/endpoints/text-generation/supported-models/mistralai-Mistral-7B-Instruct-v0.1", + "supports_function_calling": true + }, + "anyscale/mistralai/Mixtral-8x22B-Instruct-v0.1": { + "input_cost_per_token": 9e-07, + "litellm_provider": "anyscale", + "max_input_tokens": 65536, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 9e-07, + "source": "https://docs.anyscale.com/preview/endpoints/text-generation/supported-models/mistralai-Mixtral-8x22B-Instruct-v0.1", + "supports_function_calling": true + }, + "anyscale/mistralai/Mixtral-8x7B-Instruct-v0.1": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "anyscale", + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1.5e-07, + "source": "https://docs.anyscale.com/preview/endpoints/text-generation/supported-models/mistralai-Mixtral-8x7B-Instruct-v0.1", + "supports_function_calling": true + }, + "apac.amazon.nova-lite-v1:0": { + "input_cost_per_token": 6.3e-08, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 300000, + "max_output_tokens": 10000, + "max_tokens": 10000, + "mode": "chat", + "output_cost_per_token": 2.52e-07, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_vision": true + }, + "apac.amazon.nova-micro-v1:0": { + "input_cost_per_token": 3.7e-08, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 10000, + "max_tokens": 10000, + "mode": "chat", + "output_cost_per_token": 1.48e-07, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true + }, + "apac.amazon.nova-pro-v1:0": { + "input_cost_per_token": 8.4e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 300000, + "max_output_tokens": 10000, + "max_tokens": 10000, + "mode": "chat", + "output_cost_per_token": 3.36e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_vision": true + }, + "apac.anthropic.claude-3-5-sonnet-20240620-v1:0": { + "input_cost_per_token": 3e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "apac.anthropic.claude-3-5-sonnet-20241022-v2:0": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "apac.anthropic.claude-3-haiku-20240307-v1:0": { + "input_cost_per_token": 2.5e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.25e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "apac.anthropic.claude-haiku-4-5-20251001-v1:0": { + "cache_creation_input_token_cost": 1.375e-06, + "cache_read_input_token_cost": 1.1e-07, + "input_cost_per_token": 1.1e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 5.5e-06, + "source": "https://aws.amazon.com/about-aws/whats-new/2025/10/claude-4-5-haiku-anthropic-amazon-bedrock", + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 346 + }, + "apac.anthropic.claude-3-sonnet-20240229-v1:0": { + "input_cost_per_token": 3e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "apac.anthropic.claude-sonnet-4-20250514-v1:0": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "assemblyai/best": { + "input_cost_per_second": 3.333e-05, + "litellm_provider": "assemblyai", + "mode": "audio_transcription", + "output_cost_per_second": 0.0 + }, + "assemblyai/nano": { + "input_cost_per_second": 0.00010278, + "litellm_provider": "assemblyai", + "mode": "audio_transcription", + "output_cost_per_second": 0.0 + }, + "au.anthropic.claude-sonnet-4-5-20250929-v1:0": { + "cache_creation_input_token_cost": 4.125e-06, + "cache_read_input_token_cost": 3.3e-07, + "input_cost_per_token": 3.3e-06, + "input_cost_per_token_above_200k_tokens": 6.6e-06, + "output_cost_per_token_above_200k_tokens": 2.475e-05, + "cache_creation_input_token_cost_above_200k_tokens": 8.25e-06, + "cache_read_input_token_cost_above_200k_tokens": 6.6e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.65e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 346 + }, + "azure/ada": { + "input_cost_per_token": 1e-07, + "litellm_provider": "azure", + "max_input_tokens": 8191, + "max_tokens": 8191, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "azure/codex-mini": { + "cache_read_input_token_cost": 3.75e-07, + "input_cost_per_token": 1.5e-06, + "litellm_provider": "azure", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "responses", + "output_cost_per_token": 6e-06, + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/command-r-plus": { + "input_cost_per_token": 3e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_function_calling": true + }, + "azure_ai/claude-haiku-4-5": { + "input_cost_per_token": 1e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 5e-06, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure_ai/claude-opus-4-1": { + "input_cost_per_token": 1.5e-05, + "litellm_provider": "azure_ai", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure_ai/claude-sonnet-4-5": { + "input_cost_per_token": 3e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/computer-use-preview": { + "input_cost_per_token": 3e-06, + "litellm_provider": "azure", + "max_input_tokens": 8192, + "max_output_tokens": 1024, + "max_tokens": 1024, + "mode": "chat", + "output_cost_per_token": 1.2e-05, + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": false, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/container": { + "code_interpreter_cost_per_session": 0.03, + "litellm_provider": "azure", + "mode": "chat" + }, + "azure/eu/gpt-4o-2024-08-06": { + "deprecation_date": "2026-02-27", + "cache_read_input_token_cost": 1.375e-06, + "input_cost_per_token": 2.75e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1.1e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/eu/gpt-4o-2024-11-20": { + "deprecation_date": "2026-03-01", + "cache_creation_input_token_cost": 1.38e-06, + "input_cost_per_token": 2.75e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1.1e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/eu/gpt-4o-mini-2024-07-18": { + "cache_read_input_token_cost": 8.3e-08, + "input_cost_per_token": 1.65e-07, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 6.6e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/eu/gpt-4o-mini-realtime-preview-2024-12-17": { + "cache_creation_input_audio_token_cost": 3.3e-07, + "cache_read_input_token_cost": 3.3e-07, + "input_cost_per_audio_token": 1.1e-05, + "input_cost_per_token": 6.6e-07, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_audio_token": 2.2e-05, + "output_cost_per_token": 2.64e-06, + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "azure/eu/gpt-4o-realtime-preview-2024-10-01": { + "cache_creation_input_audio_token_cost": 2.2e-05, + "cache_read_input_token_cost": 2.75e-06, + "input_cost_per_audio_token": 0.00011, + "input_cost_per_token": 5.5e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_audio_token": 0.00022, + "output_cost_per_token": 2.2e-05, + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "azure/eu/gpt-4o-realtime-preview-2024-12-17": { + "cache_read_input_audio_token_cost": 2.5e-06, + "cache_read_input_token_cost": 2.75e-06, + "input_cost_per_audio_token": 4.4e-05, + "input_cost_per_token": 5.5e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_audio_token": 8e-05, + "output_cost_per_token": 2.2e-05, + "supported_modalities": [ + "text", + "audio" + ], + "supported_output_modalities": [ + "text", + "audio" + ], + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "azure/eu/gpt-5-2025-08-07": { + "cache_read_input_token_cost": 1.375e-07, + "input_cost_per_token": 1.375e-06, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.1e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/eu/gpt-5-mini-2025-08-07": { + "cache_read_input_token_cost": 2.75e-08, + "input_cost_per_token": 2.75e-07, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 2.2e-06, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/eu/gpt-5.1": { + "cache_read_input_token_cost": 1.4e-07, + "input_cost_per_token": 1.38e-06, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.1e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/eu/gpt-5.1-chat": { + "cache_read_input_token_cost": 1.4e-07, + "input_cost_per_token": 1.38e-06, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.1e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/eu/gpt-5.1-codex": { + "cache_read_input_token_cost": 1.4e-07, + "input_cost_per_token": 1.38e-06, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "output_cost_per_token": 1.1e-05, + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/eu/gpt-5.1-codex-mini": { + "cache_read_input_token_cost": 2.8e-08, + "input_cost_per_token": 2.75e-07, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "output_cost_per_token": 2.2e-06, + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/eu/gpt-5-nano-2025-08-07": { + "cache_read_input_token_cost": 5.5e-09, + "input_cost_per_token": 5.5e-08, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 4.4e-07, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/eu/o1-2024-12-17": { + "cache_read_input_token_cost": 8.25e-06, + "input_cost_per_token": 1.65e-05, + "litellm_provider": "azure", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 6.6e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/eu/o1-mini-2024-09-12": { + "cache_read_input_token_cost": 6.05e-07, + "input_cost_per_token": 1.21e-06, + "input_cost_per_token_batches": 6.05e-07, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 4.84e-06, + "output_cost_per_token_batches": 2.42e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_vision": false + }, + "azure/eu/o1-preview-2024-09-12": { + "cache_read_input_token_cost": 8.25e-06, + "input_cost_per_token": 1.65e-05, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 6.6e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_vision": false + }, + "azure/eu/o3-mini-2025-01-31": { + "cache_read_input_token_cost": 6.05e-07, + "input_cost_per_token": 1.21e-06, + "input_cost_per_token_batches": 6.05e-07, + "litellm_provider": "azure", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 4.84e-06, + "output_cost_per_token_batches": 2.42e-06, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "azure/global-standard/gpt-4o-2024-08-06": { + "cache_read_input_token_cost": 1.25e-06, + "deprecation_date": "2026-02-27", + "input_cost_per_token": 2.5e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/global-standard/gpt-4o-2024-11-20": { + "cache_read_input_token_cost": 1.25e-06, + "deprecation_date": "2026-03-01", + "input_cost_per_token": 2.5e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/global-standard/gpt-4o-mini": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 6e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/global/gpt-4o-2024-08-06": { + "deprecation_date": "2026-02-27", + "cache_read_input_token_cost": 1.25e-06, + "input_cost_per_token": 2.5e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/global/gpt-4o-2024-11-20": { + "deprecation_date": "2026-03-01", + "cache_read_input_token_cost": 1.25e-06, + "input_cost_per_token": 2.5e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/global/gpt-5.1": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/global/gpt-5.1-chat": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/global/gpt-5.1-codex": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/global/gpt-5.1-codex-mini": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_token": 2.5e-07, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "output_cost_per_token": 2e-06, + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-3.5-turbo": { + "input_cost_per_token": 5e-07, + "litellm_provider": "azure", + "max_input_tokens": 4097, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "azure/gpt-3.5-turbo-0125": { + "deprecation_date": "2025-03-31", + "input_cost_per_token": 5e-07, + "litellm_provider": "azure", + "max_input_tokens": 16384, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "azure/gpt-3.5-turbo-instruct-0914": { + "input_cost_per_token": 1.5e-06, + "litellm_provider": "azure_text", + "max_input_tokens": 4097, + "max_tokens": 4097, + "mode": "completion", + "output_cost_per_token": 2e-06 + }, + "azure/gpt-35-turbo": { + "input_cost_per_token": 5e-07, + "litellm_provider": "azure", + "max_input_tokens": 4097, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "azure/gpt-35-turbo-0125": { + "deprecation_date": "2025-05-31", + "input_cost_per_token": 5e-07, + "litellm_provider": "azure", + "max_input_tokens": 16384, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "azure/gpt-35-turbo-0301": { + "deprecation_date": "2025-02-13", + "input_cost_per_token": 2e-07, + "litellm_provider": "azure", + "max_input_tokens": 4097, + "max_output_tokens": 4096, + "max_tokens": 4097, + "mode": "chat", + "output_cost_per_token": 2e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "azure/gpt-35-turbo-0613": { + "deprecation_date": "2025-02-13", + "input_cost_per_token": 1.5e-06, + "litellm_provider": "azure", + "max_input_tokens": 4097, + "max_output_tokens": 4096, + "max_tokens": 4097, + "mode": "chat", + "output_cost_per_token": 2e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "azure/gpt-35-turbo-1106": { + "deprecation_date": "2025-03-31", + "input_cost_per_token": 1e-06, + "litellm_provider": "azure", + "max_input_tokens": 16384, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 2e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "azure/gpt-35-turbo-16k": { + "input_cost_per_token": 3e-06, + "litellm_provider": "azure", + "max_input_tokens": 16385, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 4e-06, + "supports_tool_choice": true + }, + "azure/gpt-35-turbo-16k-0613": { + "input_cost_per_token": 3e-06, + "litellm_provider": "azure", + "max_input_tokens": 16385, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 4e-06, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "azure/gpt-35-turbo-instruct": { + "input_cost_per_token": 1.5e-06, + "litellm_provider": "azure_text", + "max_input_tokens": 4097, + "max_tokens": 4097, + "mode": "completion", + "output_cost_per_token": 2e-06 + }, + "azure/gpt-35-turbo-instruct-0914": { + "input_cost_per_token": 1.5e-06, + "litellm_provider": "azure_text", + "max_input_tokens": 4097, + "max_tokens": 4097, + "mode": "completion", + "output_cost_per_token": 2e-06 + }, + "azure/gpt-4": { + "input_cost_per_token": 3e-05, + "litellm_provider": "azure", + "max_input_tokens": 8192, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 6e-05, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "azure/gpt-4-0125-preview": { + "input_cost_per_token": 1e-05, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 3e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "azure/gpt-4-0613": { + "input_cost_per_token": 3e-05, + "litellm_provider": "azure", + "max_input_tokens": 8192, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 6e-05, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "azure/gpt-4-1106-preview": { + "input_cost_per_token": 1e-05, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 3e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "azure/gpt-4-32k": { + "input_cost_per_token": 6e-05, + "litellm_provider": "azure", + "max_input_tokens": 32768, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 0.00012, + "supports_tool_choice": true + }, + "azure/gpt-4-32k-0613": { + "input_cost_per_token": 6e-05, + "litellm_provider": "azure", + "max_input_tokens": 32768, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 0.00012, + "supports_tool_choice": true + }, + "azure/gpt-4-turbo": { + "input_cost_per_token": 1e-05, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 3e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "azure/gpt-4-turbo-2024-04-09": { + "input_cost_per_token": 1e-05, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 3e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-4-turbo-vision-preview": { + "input_cost_per_token": 1e-05, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 3e-05, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-4.1": { + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 2e-06, + "input_cost_per_token_batches": 1e-06, + "litellm_provider": "azure", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 8e-06, + "output_cost_per_token_batches": 4e-06, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": false + }, + "azure/gpt-4.1-2025-04-14": { + "deprecation_date": "2026-11-04", + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 2e-06, + "input_cost_per_token_batches": 1e-06, + "litellm_provider": "azure", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 8e-06, + "output_cost_per_token_batches": 4e-06, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": false + }, + "azure/gpt-4.1-mini": { + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 4e-07, + "input_cost_per_token_batches": 2e-07, + "litellm_provider": "azure", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 1.6e-06, + "output_cost_per_token_batches": 8e-07, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": false + }, + "azure/gpt-4.1-mini-2025-04-14": { + "deprecation_date": "2026-11-04", + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 4e-07, + "input_cost_per_token_batches": 2e-07, + "litellm_provider": "azure", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 1.6e-06, + "output_cost_per_token_batches": 8e-07, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": false + }, + "azure/gpt-4.1-nano": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_token": 1e-07, + "input_cost_per_token_batches": 5e-08, + "litellm_provider": "azure", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 4e-07, + "output_cost_per_token_batches": 2e-07, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-4.1-nano-2025-04-14": { + "deprecation_date": "2026-11-04", + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_token": 1e-07, + "input_cost_per_token_batches": 5e-08, + "litellm_provider": "azure", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 4e-07, + "output_cost_per_token_batches": 2e-07, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-4.5-preview": { + "cache_read_input_token_cost": 3.75e-05, + "input_cost_per_token": 7.5e-05, + "input_cost_per_token_batches": 3.75e-05, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 0.00015, + "output_cost_per_token_batches": 7.5e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-4o": { + "cache_read_input_token_cost": 1.25e-06, + "input_cost_per_token": 2.5e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-4o-2024-05-13": { + "input_cost_per_token": 5e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-4o-2024-08-06": { + "deprecation_date": "2026-02-27", + "cache_read_input_token_cost": 1.25e-06, + "input_cost_per_token": 2.5e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-4o-2024-11-20": { + "deprecation_date": "2026-03-01", + "cache_read_input_token_cost": 1.25e-06, + "input_cost_per_token": 2.75e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1.1e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-audio-2025-08-28": { + "input_cost_per_audio_token": 4e-05, + "input_cost_per_token": 2.5e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_audio_token": 8e-05, + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supported_modalities": [ + "text", + "audio" + ], + "supported_output_modalities": [ + "text", + "audio" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": false, + "supports_reasoning": false, + "supports_response_schema": false, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "azure/gpt-audio-mini-2025-10-06": { + "input_cost_per_audio_token": 1e-05, + "input_cost_per_token": 6e-07, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_audio_token": 2e-05, + "output_cost_per_token": 2.4e-06, + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supported_modalities": [ + "text", + "audio" + ], + "supported_output_modalities": [ + "text", + "audio" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": false, + "supports_reasoning": false, + "supports_response_schema": false, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "azure/gpt-4o-audio-preview-2024-12-17": { + "input_cost_per_audio_token": 4e-05, + "input_cost_per_token": 2.5e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_audio_token": 8e-05, + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supported_modalities": [ + "text", + "audio" + ], + "supported_output_modalities": [ + "text", + "audio" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": false, + "supports_reasoning": false, + "supports_response_schema": false, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "azure/gpt-4o-mini": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_token": 1.65e-07, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 6.6e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-4o-mini-2024-07-18": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_token": 1.65e-07, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 6.6e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-4o-mini-audio-preview-2024-12-17": { + "input_cost_per_audio_token": 4e-05, + "input_cost_per_token": 2.5e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_audio_token": 8e-05, + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supported_modalities": [ + "text", + "audio" + ], + "supported_output_modalities": [ + "text", + "audio" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": false, + "supports_reasoning": false, + "supports_response_schema": false, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "azure/gpt-4o-mini-realtime-preview-2024-12-17": { + "cache_creation_input_audio_token_cost": 3e-07, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_audio_token": 1e-05, + "input_cost_per_token": 6e-07, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_audio_token": 2e-05, + "output_cost_per_token": 2.4e-06, + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "azure/gpt-realtime-2025-08-28": { + "cache_creation_input_audio_token_cost": 4e-06, + "cache_read_input_token_cost": 4e-06, + "input_cost_per_audio_token": 3.2e-05, + "input_cost_per_image": 5e-06, + "input_cost_per_token": 4e-06, + "litellm_provider": "azure", + "max_input_tokens": 32000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_audio_token": 6.4e-05, + "output_cost_per_token": 1.6e-05, + "supported_endpoints": [ + "/v1/realtime" + ], + "supported_modalities": [ + "text", + "image", + "audio" + ], + "supported_output_modalities": [ + "text", + "audio" + ], + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "azure/gpt-realtime-mini-2025-10-06": { + "cache_creation_input_audio_token_cost": 3e-07, + "cache_read_input_token_cost": 6e-08, + "input_cost_per_audio_token": 1e-05, + "input_cost_per_image": 8e-07, + "input_cost_per_token": 6e-07, + "litellm_provider": "azure", + "max_input_tokens": 32000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_audio_token": 2e-05, + "output_cost_per_token": 2.4e-06, + "supported_endpoints": [ + "/v1/realtime" + ], + "supported_modalities": [ + "text", + "image", + "audio" + ], + "supported_output_modalities": [ + "text", + "audio" + ], + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "azure/gpt-4o-mini-transcribe": { + "input_cost_per_audio_token": 3e-06, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "azure", + "max_input_tokens": 16000, + "max_output_tokens": 2000, + "mode": "audio_transcription", + "output_cost_per_token": 5e-06, + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "azure/gpt-4o-mini-tts": { + "input_cost_per_token": 2.5e-06, + "litellm_provider": "azure", + "mode": "audio_speech", + "output_cost_per_audio_token": 1.2e-05, + "output_cost_per_second": 0.00025, + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/audio/speech" + ], + "supported_modalities": [ + "text", + "audio" + ], + "supported_output_modalities": [ + "audio" + ] + }, + "azure/gpt-4o-realtime-preview-2024-10-01": { + "cache_creation_input_audio_token_cost": 2e-05, + "cache_read_input_token_cost": 2.5e-06, + "input_cost_per_audio_token": 0.0001, + "input_cost_per_token": 5e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_audio_token": 0.0002, + "output_cost_per_token": 2e-05, + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "azure/gpt-4o-realtime-preview-2024-12-17": { + "cache_read_input_token_cost": 2.5e-06, + "input_cost_per_audio_token": 4e-05, + "input_cost_per_token": 5e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_audio_token": 8e-05, + "output_cost_per_token": 2e-05, + "supported_modalities": [ + "text", + "audio" + ], + "supported_output_modalities": [ + "text", + "audio" + ], + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "azure/gpt-4o-transcribe": { + "input_cost_per_audio_token": 6e-06, + "input_cost_per_token": 2.5e-06, + "litellm_provider": "azure", + "max_input_tokens": 16000, + "max_output_tokens": 2000, + "mode": "audio_transcription", + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "azure/gpt-4o-transcribe-diarize": { + "input_cost_per_audio_token": 6e-06, + "input_cost_per_token": 2.5e-06, + "litellm_provider": "azure", + "max_input_tokens": 16000, + "max_output_tokens": 2000, + "mode": "audio_transcription", + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "azure/gpt-5.1-2025-11-13": { + "cache_read_input_token_cost": 1.25e-07, + "cache_read_input_token_cost_priority": 2.5e-07, + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_priority": 2.5e-06, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_priority": 2e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "azure/gpt-5.1-chat-2025-11-13": { + "cache_read_input_token_cost": 1.25e-07, + "cache_read_input_token_cost_priority": 2.5e-07, + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_priority": 2.5e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_priority": 2e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_function_calling": false, + "supports_native_streaming": true, + "supports_parallel_function_calling": false, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": false, + "supports_vision": true + }, + "azure/gpt-5.1-codex-2025-11-13": { + "cache_read_input_token_cost": 1.25e-07, + "cache_read_input_token_cost_priority": 2.5e-07, + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_priority": 2.5e-06, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "output_cost_per_token": 1e-05, + "output_cost_per_token_priority": 2e-05, + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-5.1-codex-mini-2025-11-13": { + "cache_read_input_token_cost": 2.5e-08, + "cache_read_input_token_cost_priority": 4.5e-08, + "input_cost_per_token": 2.5e-07, + "input_cost_per_token_priority": 4.5e-07, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "output_cost_per_token": 2e-06, + "output_cost_per_token_priority": 3.6e-06, + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-5": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-5-2025-08-07": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-5-chat": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "source": "https://azure.microsoft.com/en-us/blog/gpt-5-in-azure-ai-foundry-the-future-of-ai-apps-and-agents-starts-here/", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": false, + "supports_vision": true + }, + "azure/gpt-5-chat-latest": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": false, + "supports_vision": true + }, + "azure/gpt-5-codex": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-5-mini": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_token": 2.5e-07, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 2e-06, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-5-mini-2025-08-07": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_token": 2.5e-07, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 2e-06, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-5-nano": { + "cache_read_input_token_cost": 5e-09, + "input_cost_per_token": 5e-08, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-5-nano-2025-08-07": { + "cache_read_input_token_cost": 5e-09, + "input_cost_per_token": 5e-08, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-5-pro": { + "input_cost_per_token": 1.5e-05, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 400000, + "mode": "responses", + "output_cost_per_token": 0.00012, + "source": "https://learn.microsoft.com/en-us/azure/ai-foundry/foundry-models/concepts/models-sold-directly-by-azure?pivots=azure-openai&tabs=global-standard-aoai%2Cstandard-chat-completions%2Cglobal-standard#gpt-5", + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-5.1": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-5.1-chat": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-5.1-codex": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-5.1-codex-max": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "azure", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-5.1-codex-mini": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_token": 2.5e-07, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "output_cost_per_token": 2e-06, + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-5.2": { + "cache_read_input_token_cost": 1.75e-07, + "input_cost_per_token": 1.75e-06, + "litellm_provider": "azure", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.4e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-5.2-2025-12-11": { + "cache_read_input_token_cost": 1.75e-07, + "cache_read_input_token_cost_priority": 3.5e-07, + "input_cost_per_token": 1.75e-06, + "input_cost_per_token_priority": 3.5e-06, + "litellm_provider": "azure", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.4e-05, + "output_cost_per_token_priority": 2.8e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "azure/gpt-5.2-chat-2025-12-11": { + "cache_read_input_token_cost": 1.75e-07, + "cache_read_input_token_cost_priority": 3.5e-07, + "input_cost_per_token": 1.75e-06, + "input_cost_per_token_priority": 3.5e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1.4e-05, + "output_cost_per_token_priority": 2.8e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/gpt-5.2-pro": { + "input_cost_per_token": 2.1e-05, + "litellm_provider": "azure", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "output_cost_per_token": 1.68e-04, + "supported_endpoints": [ + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "azure/gpt-5.2-pro-2025-12-11": { + "input_cost_per_token": 2.1e-05, + "litellm_provider": "azure", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "output_cost_per_token": 1.68e-04, + "supported_endpoints": [ + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "azure/gpt-image-1": { + "input_cost_per_pixel": 4.0054321e-08, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/hd/1024-x-1024/dall-e-3": { + "input_cost_per_pixel": 7.629e-08, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_token": 0.0 + }, + "azure/hd/1024-x-1792/dall-e-3": { + "input_cost_per_pixel": 6.539e-08, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_token": 0.0 + }, + "azure/hd/1792-x-1024/dall-e-3": { + "input_cost_per_pixel": 6.539e-08, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_token": 0.0 + }, + "azure/high/1024-x-1024/gpt-image-1": { + "input_cost_per_pixel": 1.59263611e-07, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/high/1024-x-1536/gpt-image-1": { + "input_cost_per_pixel": 1.58945719e-07, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/high/1536-x-1024/gpt-image-1": { + "input_cost_per_pixel": 1.58945719e-07, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/low/1024-x-1024/gpt-image-1": { + "input_cost_per_pixel": 1.0490417e-08, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/low/1024-x-1536/gpt-image-1": { + "input_cost_per_pixel": 1.0172526e-08, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/low/1536-x-1024/gpt-image-1": { + "input_cost_per_pixel": 1.0172526e-08, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/medium/1024-x-1024/gpt-image-1": { + "input_cost_per_pixel": 4.0054321e-08, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/medium/1024-x-1536/gpt-image-1": { + "input_cost_per_pixel": 4.0054321e-08, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/medium/1536-x-1024/gpt-image-1": { + "input_cost_per_pixel": 4.0054321e-08, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/gpt-image-1-mini": { + "input_cost_per_pixel": 8.0566406e-09, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/low/1024-x-1024/gpt-image-1-mini": { + "input_cost_per_pixel": 2.0751953125e-09, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/low/1024-x-1536/gpt-image-1-mini": { + "input_cost_per_pixel": 2.0751953125e-09, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/low/1536-x-1024/gpt-image-1-mini": { + "input_cost_per_pixel": 2.0345052083e-09, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/medium/1024-x-1024/gpt-image-1-mini": { + "input_cost_per_pixel": 8.056640625e-09, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/medium/1024-x-1536/gpt-image-1-mini": { + "input_cost_per_pixel": 8.056640625e-09, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/medium/1536-x-1024/gpt-image-1-mini": { + "input_cost_per_pixel": 7.9752604167e-09, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/high/1024-x-1024/gpt-image-1-mini": { + "input_cost_per_pixel": 3.173828125e-08, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/high/1024-x-1536/gpt-image-1-mini": { + "input_cost_per_pixel": 3.173828125e-08, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/high/1536-x-1024/gpt-image-1-mini": { + "input_cost_per_pixel": 3.1575520833e-08, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure/mistral-large-2402": { + "input_cost_per_token": 8e-06, + "litellm_provider": "azure", + "max_input_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 2.4e-05, + "supports_function_calling": true + }, + "azure/mistral-large-latest": { + "input_cost_per_token": 8e-06, + "litellm_provider": "azure", + "max_input_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 2.4e-05, + "supports_function_calling": true + }, + "azure/o1": { + "cache_read_input_token_cost": 7.5e-06, + "input_cost_per_token": 1.5e-05, + "litellm_provider": "azure", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 6e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/o1-2024-12-17": { + "cache_read_input_token_cost": 7.5e-06, + "input_cost_per_token": 1.5e-05, + "litellm_provider": "azure", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 6e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/o1-mini": { + "cache_read_input_token_cost": 6.05e-07, + "input_cost_per_token": 1.21e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 4.84e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_vision": false + }, + "azure/o1-mini-2024-09-12": { + "cache_read_input_token_cost": 5.5e-07, + "input_cost_per_token": 1.1e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 4.4e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_vision": false + }, + "azure/o1-preview": { + "cache_read_input_token_cost": 7.5e-06, + "input_cost_per_token": 1.5e-05, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 6e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_vision": false + }, + "azure/o1-preview-2024-09-12": { + "cache_read_input_token_cost": 7.5e-06, + "input_cost_per_token": 1.5e-05, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 6e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_vision": false + }, + "azure/o3": { + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 2e-06, + "litellm_provider": "azure", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 8e-06, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": false, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/o3-2025-04-16": { + "deprecation_date": "2026-04-16", + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 2e-06, + "litellm_provider": "azure", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 8e-06, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": false, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/o3-deep-research": { + "cache_read_input_token_cost": 2.5e-06, + "input_cost_per_token": 1e-05, + "litellm_provider": "azure", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "responses", + "output_cost_per_token": 4e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "azure/o3-mini": { + "cache_read_input_token_cost": 5.5e-07, + "input_cost_per_token": 1.1e-06, + "litellm_provider": "azure", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 4.4e-06, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "azure/o3-mini-2025-01-31": { + "cache_read_input_token_cost": 5.5e-07, + "input_cost_per_token": 1.1e-06, + "litellm_provider": "azure", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 4.4e-06, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "azure/o3-pro": { + "input_cost_per_token": 2e-05, + "input_cost_per_token_batches": 1e-05, + "litellm_provider": "azure", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "responses", + "output_cost_per_token": 8e-05, + "output_cost_per_token_batches": 4e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": false, + "supports_prompt_caching": false, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/o3-pro-2025-06-10": { + "input_cost_per_token": 2e-05, + "input_cost_per_token_batches": 1e-05, + "litellm_provider": "azure", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "responses", + "output_cost_per_token": 8e-05, + "output_cost_per_token_batches": 4e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": false, + "supports_prompt_caching": false, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/o4-mini": { + "cache_read_input_token_cost": 2.75e-07, + "input_cost_per_token": 1.1e-06, + "litellm_provider": "azure", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 4.4e-06, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": false, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/o4-mini-2025-04-16": { + "cache_read_input_token_cost": 2.75e-07, + "input_cost_per_token": 1.1e-06, + "litellm_provider": "azure", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 4.4e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": false, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/standard/1024-x-1024/dall-e-2": { + "input_cost_per_pixel": 0.0, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_token": 0.0 + }, + "azure/standard/1024-x-1024/dall-e-3": { + "input_cost_per_pixel": 3.81469e-08, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_token": 0.0 + }, + "azure/standard/1024-x-1792/dall-e-3": { + "input_cost_per_pixel": 4.359e-08, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_token": 0.0 + }, + "azure/standard/1792-x-1024/dall-e-3": { + "input_cost_per_pixel": 4.359e-08, + "litellm_provider": "azure", + "mode": "image_generation", + "output_cost_per_token": 0.0 + }, + "azure/text-embedding-3-large": { + "input_cost_per_token": 1.3e-07, + "litellm_provider": "azure", + "max_input_tokens": 8191, + "max_tokens": 8191, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "azure/text-embedding-3-small": { + "deprecation_date": "2026-04-30", + "input_cost_per_token": 2e-08, + "litellm_provider": "azure", + "max_input_tokens": 8191, + "max_tokens": 8191, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "azure/text-embedding-ada-002": { + "input_cost_per_token": 1e-07, + "litellm_provider": "azure", + "max_input_tokens": 8191, + "max_tokens": 8191, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "azure/speech/azure-tts": { + "input_cost_per_character": 15e-06, + "litellm_provider": "azure", + "mode": "audio_speech", + "source": "https://azure.microsoft.com/en-us/pricing/calculator/" + }, + "azure/speech/azure-tts-hd": { + "input_cost_per_character": 30e-06, + "litellm_provider": "azure", + "mode": "audio_speech", + "source": "https://azure.microsoft.com/en-us/pricing/calculator/" + }, + "azure/tts-1": { + "input_cost_per_character": 1.5e-05, + "litellm_provider": "azure", + "mode": "audio_speech" + }, + "azure/tts-1-hd": { + "input_cost_per_character": 3e-05, + "litellm_provider": "azure", + "mode": "audio_speech" + }, + "azure/us/gpt-4.1-2025-04-14": { + "deprecation_date": "2026-11-04", + "cache_read_input_token_cost": 5.5e-07, + "input_cost_per_token": 2.2e-06, + "input_cost_per_token_batches": 1.1e-06, + "litellm_provider": "azure", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 8.8e-06, + "output_cost_per_token_batches": 4.4e-06, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": false + }, + "azure/us/gpt-4.1-mini-2025-04-14": { + "deprecation_date": "2026-11-04", + "cache_read_input_token_cost": 1.1e-07, + "input_cost_per_token": 4.4e-07, + "input_cost_per_token_batches": 2.2e-07, + "litellm_provider": "azure", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 1.76e-06, + "output_cost_per_token_batches": 8.8e-07, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": false + }, + "azure/us/gpt-4.1-nano-2025-04-14": { + "deprecation_date": "2026-11-04", + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_token": 1.1e-07, + "input_cost_per_token_batches": 6e-08, + "litellm_provider": "azure", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 4.4e-07, + "output_cost_per_token_batches": 2.2e-07, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/us/gpt-4o-2024-08-06": { + "deprecation_date": "2026-02-27", + "cache_read_input_token_cost": 1.375e-06, + "input_cost_per_token": 2.75e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1.1e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/us/gpt-4o-2024-11-20": { + "deprecation_date": "2026-03-01", + "cache_creation_input_token_cost": 1.38e-06, + "input_cost_per_token": 2.75e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1.1e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/us/gpt-4o-mini-2024-07-18": { + "cache_read_input_token_cost": 8.3e-08, + "input_cost_per_token": 1.65e-07, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 6.6e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/us/gpt-4o-mini-realtime-preview-2024-12-17": { + "cache_creation_input_audio_token_cost": 3.3e-07, + "cache_read_input_token_cost": 3.3e-07, + "input_cost_per_audio_token": 1.1e-05, + "input_cost_per_token": 6.6e-07, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_audio_token": 2.2e-05, + "output_cost_per_token": 2.64e-06, + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "azure/us/gpt-4o-realtime-preview-2024-10-01": { + "cache_creation_input_audio_token_cost": 2.2e-05, + "cache_read_input_token_cost": 2.75e-06, + "input_cost_per_audio_token": 0.00011, + "input_cost_per_token": 5.5e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_audio_token": 0.00022, + "output_cost_per_token": 2.2e-05, + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "azure/us/gpt-4o-realtime-preview-2024-12-17": { + "cache_read_input_audio_token_cost": 2.5e-06, + "cache_read_input_token_cost": 2.75e-06, + "input_cost_per_audio_token": 4.4e-05, + "input_cost_per_token": 5.5e-06, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_audio_token": 8e-05, + "output_cost_per_token": 2.2e-05, + "supported_modalities": [ + "text", + "audio" + ], + "supported_output_modalities": [ + "text", + "audio" + ], + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "azure/us/gpt-5-2025-08-07": { + "cache_read_input_token_cost": 1.375e-07, + "input_cost_per_token": 1.375e-06, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.1e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/us/gpt-5-mini-2025-08-07": { + "cache_read_input_token_cost": 2.75e-08, + "input_cost_per_token": 2.75e-07, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 2.2e-06, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/us/gpt-5-nano-2025-08-07": { + "cache_read_input_token_cost": 5.5e-09, + "input_cost_per_token": 5.5e-08, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 4.4e-07, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/us/gpt-5.1": { + "cache_read_input_token_cost": 1.4e-07, + "input_cost_per_token": 1.38e-06, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.1e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/us/gpt-5.1-chat": { + "cache_read_input_token_cost": 1.4e-07, + "input_cost_per_token": 1.38e-06, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.1e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/us/gpt-5.1-codex": { + "cache_read_input_token_cost": 1.4e-07, + "input_cost_per_token": 1.38e-06, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "output_cost_per_token": 1.1e-05, + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/us/gpt-5.1-codex-mini": { + "cache_read_input_token_cost": 2.8e-08, + "input_cost_per_token": 2.75e-07, + "litellm_provider": "azure", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "output_cost_per_token": 2.2e-06, + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/us/o1-2024-12-17": { + "cache_read_input_token_cost": 8.25e-06, + "input_cost_per_token": 1.65e-05, + "litellm_provider": "azure", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 6.6e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/us/o1-mini-2024-09-12": { + "cache_read_input_token_cost": 6.05e-07, + "input_cost_per_token": 1.21e-06, + "input_cost_per_token_batches": 6.05e-07, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 4.84e-06, + "output_cost_per_token_batches": 2.42e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_vision": false + }, + "azure/us/o1-preview-2024-09-12": { + "cache_read_input_token_cost": 8.25e-06, + "input_cost_per_token": 1.65e-05, + "litellm_provider": "azure", + "max_input_tokens": 128000, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 6.6e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_vision": false + }, + "azure/us/o3-2025-04-16": { + "deprecation_date": "2026-04-16", + "cache_read_input_token_cost": 5.5e-07, + "input_cost_per_token": 2.2e-06, + "litellm_provider": "azure", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 8.8e-06, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": false, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/us/o3-mini-2025-01-31": { + "cache_read_input_token_cost": 6.05e-07, + "input_cost_per_token": 1.21e-06, + "input_cost_per_token_batches": 6.05e-07, + "litellm_provider": "azure", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 4.84e-06, + "output_cost_per_token_batches": 2.42e-06, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "azure/us/o4-mini-2025-04-16": { + "cache_read_input_token_cost": 3.1e-07, + "input_cost_per_token": 1.21e-06, + "litellm_provider": "azure", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 4.84e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": false, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure/whisper-1": { + "input_cost_per_second": 0.0001, + "litellm_provider": "azure", + "mode": "audio_transcription", + "output_cost_per_second": 0.0001 + }, + "azure_ai/Cohere-embed-v3-english": { + "input_cost_per_token": 1e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 512, + "max_tokens": 512, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 1024, + "source": "https://azuremarketplace.microsoft.com/en-us/marketplace/apps/cohere.cohere-embed-v3-english-offer?tab=PlansAndPrice", + "supports_embedding_image_input": true + }, + "azure_ai/Cohere-embed-v3-multilingual": { + "input_cost_per_token": 1e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 512, + "max_tokens": 512, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 1024, + "source": "https://azuremarketplace.microsoft.com/en-us/marketplace/apps/cohere.cohere-embed-v3-english-offer?tab=PlansAndPrice", + "supports_embedding_image_input": true + }, + "azure_ai/FLUX-1.1-pro": { + "litellm_provider": "azure_ai", + "mode": "image_generation", + "output_cost_per_image": 0.04, + "source": "https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/black-forest-labs-flux-1-kontext-pro-and-flux1-1-pro-now-available-in-azure-ai-f/4434659", + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure_ai/FLUX.1-Kontext-pro": { + "litellm_provider": "azure_ai", + "mode": "image_generation", + "output_cost_per_image": 0.04, + "source": "https://azuremarketplace.microsoft.com/pt-br/marketplace/apps/cohere.cohere-embed-4-offer?tab=PlansAndPrice", + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "azure_ai/Llama-3.2-11B-Vision-Instruct": { + "input_cost_per_token": 3.7e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 128000, + "max_output_tokens": 2048, + "max_tokens": 2048, + "mode": "chat", + "output_cost_per_token": 3.7e-07, + "source": "https://azuremarketplace.microsoft.com/en/marketplace/apps/metagenai.meta-llama-3-2-11b-vision-instruct-offer?tab=Overview", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure_ai/Llama-3.2-90B-Vision-Instruct": { + "input_cost_per_token": 2.04e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 128000, + "max_output_tokens": 2048, + "max_tokens": 2048, + "mode": "chat", + "output_cost_per_token": 2.04e-06, + "source": "https://azuremarketplace.microsoft.com/en/marketplace/apps/metagenai.meta-llama-3-2-90b-vision-instruct-offer?tab=Overview", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure_ai/Llama-3.3-70B-Instruct": { + "input_cost_per_token": 7.1e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 128000, + "max_output_tokens": 2048, + "max_tokens": 2048, + "mode": "chat", + "output_cost_per_token": 7.1e-07, + "source": "https://azuremarketplace.microsoft.com/en/marketplace/apps/metagenai.llama-3-3-70b-instruct-offer?tab=Overview", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "azure_ai/Llama-4-Maverick-17B-128E-Instruct-FP8": { + "input_cost_per_token": 1.41e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 1000000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 3.5e-07, + "source": "https://azure.microsoft.com/en-us/blog/introducing-the-llama-4-herd-in-azure-ai-foundry-and-azure-databricks/", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure_ai/Llama-4-Scout-17B-16E-Instruct": { + "input_cost_per_token": 2e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 10000000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 7.8e-07, + "source": "https://azure.microsoft.com/en-us/blog/introducing-the-llama-4-herd-in-azure-ai-foundry-and-azure-databricks/", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure_ai/Meta-Llama-3-70B-Instruct": { + "input_cost_per_token": 1.1e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 8192, + "max_output_tokens": 2048, + "max_tokens": 2048, + "mode": "chat", + "output_cost_per_token": 3.7e-07, + "supports_tool_choice": true + }, + "azure_ai/Meta-Llama-3.1-405B-Instruct": { + "input_cost_per_token": 5.33e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 128000, + "max_output_tokens": 2048, + "max_tokens": 2048, + "mode": "chat", + "output_cost_per_token": 1.6e-05, + "source": "https://azuremarketplace.microsoft.com/en-us/marketplace/apps/metagenai.meta-llama-3-1-405b-instruct-offer?tab=PlansAndPrice", + "supports_tool_choice": true + }, + "azure_ai/Meta-Llama-3.1-70B-Instruct": { + "input_cost_per_token": 2.68e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 128000, + "max_output_tokens": 2048, + "max_tokens": 2048, + "mode": "chat", + "output_cost_per_token": 3.54e-06, + "source": "https://azuremarketplace.microsoft.com/en-us/marketplace/apps/metagenai.meta-llama-3-1-70b-instruct-offer?tab=PlansAndPrice", + "supports_tool_choice": true + }, + "azure_ai/Meta-Llama-3.1-8B-Instruct": { + "input_cost_per_token": 3e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 128000, + "max_output_tokens": 2048, + "max_tokens": 2048, + "mode": "chat", + "output_cost_per_token": 6.1e-07, + "source": "https://azuremarketplace.microsoft.com/en-us/marketplace/apps/metagenai.meta-llama-3-1-8b-instruct-offer?tab=PlansAndPrice", + "supports_tool_choice": true + }, + "azure_ai/Phi-3-medium-128k-instruct": { + "input_cost_per_token": 1.7e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 6.8e-07, + "source": "https://azure.microsoft.com/en-us/pricing/details/phi-3/", + "supports_tool_choice": true, + "supports_vision": false + }, + "azure_ai/Phi-3-medium-4k-instruct": { + "input_cost_per_token": 1.7e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 6.8e-07, + "source": "https://azure.microsoft.com/en-us/pricing/details/phi-3/", + "supports_tool_choice": true, + "supports_vision": false + }, + "azure_ai/Phi-3-mini-128k-instruct": { + "input_cost_per_token": 1.3e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 5.2e-07, + "source": "https://azure.microsoft.com/en-us/pricing/details/phi-3/", + "supports_tool_choice": true, + "supports_vision": false + }, + "azure_ai/Phi-3-mini-4k-instruct": { + "input_cost_per_token": 1.3e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 5.2e-07, + "source": "https://azure.microsoft.com/en-us/pricing/details/phi-3/", + "supports_tool_choice": true, + "supports_vision": false + }, + "azure_ai/Phi-3-small-128k-instruct": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 6e-07, + "source": "https://azure.microsoft.com/en-us/pricing/details/phi-3/", + "supports_tool_choice": true, + "supports_vision": false + }, + "azure_ai/Phi-3-small-8k-instruct": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 8192, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 6e-07, + "source": "https://azure.microsoft.com/en-us/pricing/details/phi-3/", + "supports_tool_choice": true, + "supports_vision": false + }, + "azure_ai/Phi-3.5-MoE-instruct": { + "input_cost_per_token": 1.6e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 6.4e-07, + "source": "https://azure.microsoft.com/en-us/pricing/details/phi-3/", + "supports_tool_choice": true, + "supports_vision": false + }, + "azure_ai/Phi-3.5-mini-instruct": { + "input_cost_per_token": 1.3e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 5.2e-07, + "source": "https://azure.microsoft.com/en-us/pricing/details/phi-3/", + "supports_tool_choice": true, + "supports_vision": false + }, + "azure_ai/Phi-3.5-vision-instruct": { + "input_cost_per_token": 1.3e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 5.2e-07, + "source": "https://azure.microsoft.com/en-us/pricing/details/phi-3/", + "supports_tool_choice": true, + "supports_vision": true + }, + "azure_ai/Phi-4": { + "input_cost_per_token": 1.25e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 5e-07, + "source": "https://techcommunity.microsoft.com/blog/machinelearningblog/affordable-innovation-unveiling-the-pricing-of-phi-3-slms-on-models-as-a-service/4156495", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "azure_ai/Phi-4-mini-instruct": { + "input_cost_per_token": 7.5e-08, + "litellm_provider": "azure_ai", + "max_input_tokens": 131072, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 3e-07, + "source": "https://techcommunity.microsoft.com/blog/Azure-AI-Services-blog/announcing-new-phi-pricing-empowering-your-business-with-small-language-models/4395112", + "supports_function_calling": true + }, + "azure_ai/Phi-4-multimodal-instruct": { + "input_cost_per_audio_token": 4e-06, + "input_cost_per_token": 8e-08, + "litellm_provider": "azure_ai", + "max_input_tokens": 131072, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 3.2e-07, + "source": "https://techcommunity.microsoft.com/blog/Azure-AI-Services-blog/announcing-new-phi-pricing-empowering-your-business-with-small-language-models/4395112", + "supports_audio_input": true, + "supports_function_calling": true, + "supports_vision": true + }, + "azure_ai/Phi-4-mini-reasoning": { + "input_cost_per_token": 8e-08, + "litellm_provider": "azure_ai", + "max_input_tokens": 131072, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 3.2e-07, + "source": "https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/microsoft/", + "supports_function_calling": true + }, + "azure_ai/Phi-4-reasoning": { + "input_cost_per_token": 1.25e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 32768, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 5e-07, + "source": "https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/microsoft/", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_reasoning": true + }, + "azure_ai/mistral-document-ai-2505": { + "litellm_provider": "azure_ai", + "ocr_cost_per_page": 3e-3, + "mode": "ocr", + "supported_endpoints": [ + "/v1/ocr" + ], + "source": "https://devblogs.microsoft.com/foundry/whats-new-in-azure-ai-foundry-august-2025/#mistral-document-ai-(ocr)-%E2%80%94-serverless-in-foundry" + }, + "azure_ai/doc-intelligence/prebuilt-read": { + "litellm_provider": "azure_ai", + "ocr_cost_per_page": 1.5e-3, + "mode": "ocr", + "supported_endpoints": [ + "/v1/ocr" + ], + "source": "https://azure.microsoft.com/en-us/pricing/details/ai-document-intelligence/" + }, + "azure_ai/doc-intelligence/prebuilt-layout": { + "litellm_provider": "azure_ai", + "ocr_cost_per_page": 1e-2, + "mode": "ocr", + "supported_endpoints": [ + "/v1/ocr" + ], + "source": "https://azure.microsoft.com/en-us/pricing/details/ai-document-intelligence/" + }, + "azure_ai/doc-intelligence/prebuilt-document": { + "litellm_provider": "azure_ai", + "ocr_cost_per_page": 1e-2, + "mode": "ocr", + "supported_endpoints": [ + "/v1/ocr" + ], + "source": "https://azure.microsoft.com/en-us/pricing/details/ai-document-intelligence/" + }, + "azure_ai/MAI-DS-R1": { + "input_cost_per_token": 1.35e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 5.4e-06, + "source": "https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/microsoft/", + "supports_reasoning": true, + "supports_tool_choice": true + }, + "azure_ai/cohere-rerank-v3-english": { + "input_cost_per_query": 0.002, + "input_cost_per_token": 0.0, + "litellm_provider": "azure_ai", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_query_tokens": 2048, + "max_tokens": 4096, + "mode": "rerank", + "output_cost_per_token": 0.0 + }, + "azure_ai/cohere-rerank-v3-multilingual": { + "input_cost_per_query": 0.002, + "input_cost_per_token": 0.0, + "litellm_provider": "azure_ai", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_query_tokens": 2048, + "max_tokens": 4096, + "mode": "rerank", + "output_cost_per_token": 0.0 + }, + "azure_ai/cohere-rerank-v3.5": { + "input_cost_per_query": 0.002, + "input_cost_per_token": 0.0, + "litellm_provider": "azure_ai", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_query_tokens": 2048, + "max_tokens": 4096, + "mode": "rerank", + "output_cost_per_token": 0.0 + }, + "azure_ai/cohere-rerank-v4.0-pro": { + "input_cost_per_query": 0.0025, + "input_cost_per_token": 0.0, + "litellm_provider": "azure_ai", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_query_tokens": 4096, + "max_tokens": 32768, + "mode": "rerank", + "output_cost_per_token": 0.0 + }, + "azure_ai/cohere-rerank-v4.0-fast": { + "input_cost_per_query": 0.002, + "input_cost_per_token": 0.0, + "litellm_provider": "azure_ai", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_query_tokens": 4096, + "max_tokens": 32768, + "mode": "rerank", + "output_cost_per_token": 0.0 + }, + "azure_ai/deepseek-v3.2": { + "input_cost_per_token": 5.8e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.68e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "azure_ai/deepseek-v3.2-speciale": { + "input_cost_per_token": 5.8e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.68e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "azure_ai/deepseek-r1": { + "input_cost_per_token": 1.35e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 5.4e-06, + "source": "https://techcommunity.microsoft.com/blog/machinelearningblog/deepseek-r1-improved-performance-higher-limits-and-transparent-pricing/4386367", + "supports_reasoning": true, + "supports_tool_choice": true + }, + "azure_ai/deepseek-v3": { + "input_cost_per_token": 1.14e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 4.56e-06, + "source": "https://techcommunity.microsoft.com/blog/machinelearningblog/announcing-deepseek-v3-on-azure-ai-foundry-and-github/4390438", + "supports_tool_choice": true + }, + "azure_ai/deepseek-v3-0324": { + "input_cost_per_token": 1.14e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 4.56e-06, + "source": "https://techcommunity.microsoft.com/blog/machinelearningblog/announcing-deepseek-v3-on-azure-ai-foundry-and-github/4390438", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "azure_ai/embed-v-4-0": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 128000, + "max_tokens": 128000, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 3072, + "source": "https://azuremarketplace.microsoft.com/pt-br/marketplace/apps/cohere.cohere-embed-4-offer?tab=PlansAndPrice", + "supported_endpoints": [ + "/v1/embeddings" + ], + "supported_modalities": [ + "text", + "image" + ], + "supports_embedding_image_input": true + }, + "azure_ai/global/grok-3": { + "input_cost_per_token": 3e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "source": "https://devblogs.microsoft.com/foundry/announcing-grok-3-and-grok-3-mini-on-azure-ai-foundry/", + "supports_function_calling": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_web_search": true + }, + "azure_ai/global/grok-3-mini": { + "input_cost_per_token": 2.5e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1.27e-06, + "source": "https://devblogs.microsoft.com/foundry/announcing-grok-3-and-grok-3-mini-on-azure-ai-foundry/", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_web_search": true + }, + "azure_ai/grok-3": { + "input_cost_per_token": 3.3e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1.65e-05, + "source": "https://devblogs.microsoft.com/foundry/announcing-grok-3-and-grok-3-mini-on-azure-ai-foundry/", + "supports_function_calling": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_web_search": true + }, + "azure_ai/grok-3-mini": { + "input_cost_per_token": 2.75e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1.38e-06, + "source": "https://devblogs.microsoft.com/foundry/announcing-grok-3-and-grok-3-mini-on-azure-ai-foundry/", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_web_search": true + }, + "azure_ai/grok-4": { + "input_cost_per_token": 5.5e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2.75e-05, + "source": "https://azure.microsoft.com/en-us/blog/grok-4-is-now-available-in-azure-ai-foundry-unlock-frontier-intelligence-and-business-ready-capabilities/", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "azure_ai/grok-4-fast-non-reasoning": { + "input_cost_per_token": 0.43e-06, + "output_cost_per_token": 1.73e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "azure_ai/grok-4-fast-reasoning": { + "input_cost_per_token": 0.43e-06, + "output_cost_per_token": 1.73e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "source": "https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/announcing-the-grok-4-fast-models-from-xai-now-available-in-azure-ai-foundry/4456701", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "azure_ai/grok-code-fast-1": { + "input_cost_per_token": 3.5e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1.75e-05, + "source": "https://azure.microsoft.com/en-us/blog/grok-4-is-now-available-in-azure-ai-foundry-unlock-frontier-intelligence-and-business-ready-capabilities/", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "azure_ai/jais-30b-chat": { + "input_cost_per_token": 0.0032, + "litellm_provider": "azure_ai", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 0.00971, + "source": "https://azure.microsoft.com/en-us/products/ai-services/ai-foundry/models/jais-30b-chat" + }, + "azure_ai/jamba-instruct": { + "input_cost_per_token": 5e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 70000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 7e-07, + "supports_tool_choice": true + }, + "azure_ai/ministral-3b": { + "input_cost_per_token": 4e-08, + "litellm_provider": "azure_ai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 4e-08, + "source": "https://azuremarketplace.microsoft.com/en/marketplace/apps/000-000.ministral-3b-2410-offer?tab=Overview", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "azure_ai/mistral-large": { + "input_cost_per_token": 4e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 1.2e-05, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "azure_ai/mistral-large-2407": { + "input_cost_per_token": 2e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 6e-06, + "source": "https://azuremarketplace.microsoft.com/en/marketplace/apps/000-000.mistral-ai-large-2407-offer?tab=Overview", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "azure_ai/mistral-large-latest": { + "input_cost_per_token": 2e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 6e-06, + "source": "https://azuremarketplace.microsoft.com/en/marketplace/apps/000-000.mistral-ai-large-2407-offer?tab=Overview", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "azure_ai/mistral-large-3": { + "input_cost_per_token": 5e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 256000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "source": "https://azure.microsoft.com/en-us/blog/introducing-mistral-large-3-in-microsoft-foundry-open-capable-and-ready-for-production-workloads/", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "azure_ai/mistral-medium-2505": { + "input_cost_per_token": 4e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 131072, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "azure_ai/mistral-nemo": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "azure_ai", + "max_input_tokens": 131072, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-07, + "source": "https://azuremarketplace.microsoft.com/en/marketplace/apps/000-000.mistral-nemo-12b-2407?tab=PlansAndPrice", + "supports_function_calling": true + }, + "azure_ai/mistral-small": { + "input_cost_per_token": 1e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 3e-06, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "azure_ai/mistral-small-2503": { + "input_cost_per_token": 1e-06, + "litellm_provider": "azure_ai", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 3e-06, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "babbage-002": { + "input_cost_per_token": 4e-07, + "litellm_provider": "text-completion-openai", + "max_input_tokens": 16384, + "max_output_tokens": 4096, + "max_tokens": 16384, + "mode": "completion", + "output_cost_per_token": 4e-07 + }, + "bedrock/*/1-month-commitment/cohere.command-light-text-v14": { + "input_cost_per_second": 0.001902, + "litellm_provider": "bedrock", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_second": 0.001902, + "supports_tool_choice": true + }, + "bedrock/*/1-month-commitment/cohere.command-text-v14": { + "input_cost_per_second": 0.011, + "litellm_provider": "bedrock", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_second": 0.011, + "supports_tool_choice": true + }, + "bedrock/*/6-month-commitment/cohere.command-light-text-v14": { + "input_cost_per_second": 0.0011416, + "litellm_provider": "bedrock", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_second": 0.0011416, + "supports_tool_choice": true + }, + "bedrock/*/6-month-commitment/cohere.command-text-v14": { + "input_cost_per_second": 0.0066027, + "litellm_provider": "bedrock", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_second": 0.0066027, + "supports_tool_choice": true + }, + "bedrock/ap-northeast-1/1-month-commitment/anthropic.claude-instant-v1": { + "input_cost_per_second": 0.01475, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_second": 0.01475, + "supports_tool_choice": true + }, + "bedrock/ap-northeast-1/1-month-commitment/anthropic.claude-v1": { + "input_cost_per_second": 0.0455, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_second": 0.0455 + }, + "bedrock/ap-northeast-1/1-month-commitment/anthropic.claude-v2:1": { + "input_cost_per_second": 0.0455, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_second": 0.0455, + "supports_tool_choice": true + }, + "bedrock/ap-northeast-1/6-month-commitment/anthropic.claude-instant-v1": { + "input_cost_per_second": 0.008194, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_second": 0.008194, + "supports_tool_choice": true + }, + "bedrock/ap-northeast-1/6-month-commitment/anthropic.claude-v1": { + "input_cost_per_second": 0.02527, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_second": 0.02527 + }, + "bedrock/ap-northeast-1/6-month-commitment/anthropic.claude-v2:1": { + "input_cost_per_second": 0.02527, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_second": 0.02527, + "supports_tool_choice": true + }, + "bedrock/ap-northeast-1/anthropic.claude-instant-v1": { + "input_cost_per_token": 2.23e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 7.55e-06, + "supports_tool_choice": true + }, + "bedrock/ap-northeast-1/anthropic.claude-v1": { + "input_cost_per_token": 8e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2.4e-05, + "supports_tool_choice": true + }, + "bedrock/ap-northeast-1/anthropic.claude-v2:1": { + "input_cost_per_token": 8e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2.4e-05, + "supports_tool_choice": true + }, + "bedrock/ap-south-1/meta.llama3-70b-instruct-v1:0": { + "input_cost_per_token": 3.18e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 4.2e-06 + }, + "bedrock/ap-south-1/meta.llama3-8b-instruct-v1:0": { + "input_cost_per_token": 3.6e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 7.2e-07 + }, + "bedrock/ca-central-1/meta.llama3-70b-instruct-v1:0": { + "input_cost_per_token": 3.05e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 4.03e-06 + }, + "bedrock/ca-central-1/meta.llama3-8b-instruct-v1:0": { + "input_cost_per_token": 3.5e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 6.9e-07 + }, + "bedrock/eu-central-1/1-month-commitment/anthropic.claude-instant-v1": { + "input_cost_per_second": 0.01635, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_second": 0.01635, + "supports_tool_choice": true + }, + "bedrock/eu-central-1/1-month-commitment/anthropic.claude-v1": { + "input_cost_per_second": 0.0415, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_second": 0.0415 + }, + "bedrock/eu-central-1/1-month-commitment/anthropic.claude-v2:1": { + "input_cost_per_second": 0.0415, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_second": 0.0415, + "supports_tool_choice": true + }, + "bedrock/eu-central-1/6-month-commitment/anthropic.claude-instant-v1": { + "input_cost_per_second": 0.009083, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_second": 0.009083, + "supports_tool_choice": true + }, + "bedrock/eu-central-1/6-month-commitment/anthropic.claude-v1": { + "input_cost_per_second": 0.02305, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_second": 0.02305 + }, + "bedrock/eu-central-1/6-month-commitment/anthropic.claude-v2:1": { + "input_cost_per_second": 0.02305, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_second": 0.02305, + "supports_tool_choice": true + }, + "bedrock/eu-central-1/anthropic.claude-instant-v1": { + "input_cost_per_token": 2.48e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 8.38e-06, + "supports_tool_choice": true + }, + "bedrock/eu-central-1/anthropic.claude-v1": { + "input_cost_per_token": 8e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2.4e-05 + }, + "bedrock/eu-central-1/anthropic.claude-v2:1": { + "input_cost_per_token": 8e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2.4e-05, + "supports_tool_choice": true + }, + "bedrock/eu-west-1/meta.llama3-70b-instruct-v1:0": { + "input_cost_per_token": 2.86e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 3.78e-06 + }, + "bedrock/eu-west-1/meta.llama3-8b-instruct-v1:0": { + "input_cost_per_token": 3.2e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 6.5e-07 + }, + "bedrock/eu-west-2/meta.llama3-70b-instruct-v1:0": { + "input_cost_per_token": 3.45e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 4.55e-06 + }, + "bedrock/eu-west-2/meta.llama3-8b-instruct-v1:0": { + "input_cost_per_token": 3.9e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 7.8e-07 + }, + "bedrock/eu-west-3/mistral.mistral-7b-instruct-v0:2": { + "input_cost_per_token": 2e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2.6e-07, + "supports_tool_choice": true + }, + "bedrock/eu-west-3/mistral.mistral-large-2402-v1:0": { + "input_cost_per_token": 1.04e-05, + "litellm_provider": "bedrock", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 3.12e-05, + "supports_function_calling": true + }, + "bedrock/eu-west-3/mistral.mixtral-8x7b-instruct-v0:1": { + "input_cost_per_token": 5.9e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 9.1e-07, + "supports_tool_choice": true + }, + "bedrock/invoke/anthropic.claude-3-5-sonnet-20240620-v1:0": { + "input_cost_per_token": 3e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "metadata": { + "notes": "Anthropic via Invoke route does not currently support pdf input." + }, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "bedrock/sa-east-1/meta.llama3-70b-instruct-v1:0": { + "input_cost_per_token": 4.45e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 5.88e-06 + }, + "bedrock/sa-east-1/meta.llama3-8b-instruct-v1:0": { + "input_cost_per_token": 5e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.01e-06 + }, + "bedrock/us-east-1/1-month-commitment/anthropic.claude-instant-v1": { + "input_cost_per_second": 0.011, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_second": 0.011, + "supports_tool_choice": true + }, + "bedrock/us-east-1/1-month-commitment/anthropic.claude-v1": { + "input_cost_per_second": 0.0175, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_second": 0.0175 + }, + "bedrock/us-east-1/1-month-commitment/anthropic.claude-v2:1": { + "input_cost_per_second": 0.0175, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_second": 0.0175, + "supports_tool_choice": true + }, + "bedrock/us-east-1/6-month-commitment/anthropic.claude-instant-v1": { + "input_cost_per_second": 0.00611, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_second": 0.00611, + "supports_tool_choice": true + }, + "bedrock/us-east-1/6-month-commitment/anthropic.claude-v1": { + "input_cost_per_second": 0.00972, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_second": 0.00972 + }, + "bedrock/us-east-1/6-month-commitment/anthropic.claude-v2:1": { + "input_cost_per_second": 0.00972, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_second": 0.00972, + "supports_tool_choice": true + }, + "bedrock/us-east-1/anthropic.claude-instant-v1": { + "input_cost_per_token": 8e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2.4e-06, + "supports_tool_choice": true + }, + "bedrock/us-east-1/anthropic.claude-v1": { + "input_cost_per_token": 8e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2.4e-05, + "supports_tool_choice": true + }, + "bedrock/us-east-1/anthropic.claude-v2:1": { + "input_cost_per_token": 8e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2.4e-05, + "supports_tool_choice": true + }, + "bedrock/us-east-1/meta.llama3-70b-instruct-v1:0": { + "input_cost_per_token": 2.65e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 3.5e-06 + }, + "bedrock/us-east-1/meta.llama3-8b-instruct-v1:0": { + "input_cost_per_token": 3e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 6e-07 + }, + "bedrock/us-east-1/mistral.mistral-7b-instruct-v0:2": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2e-07, + "supports_tool_choice": true + }, + "bedrock/us-east-1/mistral.mistral-large-2402-v1:0": { + "input_cost_per_token": 8e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2.4e-05, + "supports_function_calling": true + }, + "bedrock/us-east-1/mistral.mixtral-8x7b-instruct-v0:1": { + "input_cost_per_token": 4.5e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 7e-07, + "supports_tool_choice": true + }, + "bedrock/us-gov-east-1/amazon.nova-pro-v1:0": { + "input_cost_per_token": 9.6e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 300000, + "max_output_tokens": 10000, + "max_tokens": 10000, + "mode": "chat", + "output_cost_per_token": 3.84e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_vision": true + }, + "bedrock/us-gov-east-1/amazon.titan-embed-text-v1": { + "input_cost_per_token": 1e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 8192, + "max_tokens": 8192, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 1536 + }, + "bedrock/us-gov-east-1/amazon.titan-embed-text-v2:0": { + "input_cost_per_token": 2e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 8192, + "max_tokens": 8192, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 1024 + }, + "bedrock/us-gov-east-1/amazon.titan-text-express-v1": { + "input_cost_per_token": 1.3e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 42000, + "max_output_tokens": 8000, + "max_tokens": 8000, + "mode": "chat", + "output_cost_per_token": 1.7e-06 + }, + "bedrock/us-gov-east-1/amazon.titan-text-lite-v1": { + "input_cost_per_token": 3e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 42000, + "max_output_tokens": 4000, + "max_tokens": 4000, + "mode": "chat", + "output_cost_per_token": 4e-07 + }, + "bedrock/us-gov-east-1/amazon.titan-text-premier-v1:0": { + "input_cost_per_token": 5e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 42000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 1.5e-06 + }, + "bedrock/us-gov-east-1/anthropic.claude-3-5-sonnet-20240620-v1:0": { + "input_cost_per_token": 3.6e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.8e-05, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "bedrock/us-gov-east-1/anthropic.claude-3-haiku-20240307-v1:0": { + "input_cost_per_token": 3e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "bedrock/us-gov-east-1/claude-sonnet-4-5-20250929-v1:0": { + "input_cost_per_token": 3.3e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.65e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "bedrock/us-gov-east-1/meta.llama3-70b-instruct-v1:0": { + "input_cost_per_token": 2.65e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 8000, + "max_output_tokens": 2048, + "max_tokens": 2048, + "mode": "chat", + "output_cost_per_token": 3.5e-06, + "supports_pdf_input": true + }, + "bedrock/us-gov-east-1/meta.llama3-8b-instruct-v1:0": { + "input_cost_per_token": 3e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 8000, + "max_output_tokens": 2048, + "max_tokens": 2048, + "mode": "chat", + "output_cost_per_token": 2.65e-06, + "supports_pdf_input": true + }, + "bedrock/us-gov-west-1/amazon.nova-pro-v1:0": { + "input_cost_per_token": 9.6e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 300000, + "max_output_tokens": 10000, + "max_tokens": 10000, + "mode": "chat", + "output_cost_per_token": 3.84e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_vision": true + }, + "bedrock/us-gov-west-1/amazon.titan-embed-text-v1": { + "input_cost_per_token": 1e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 8192, + "max_tokens": 8192, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 1536 + }, + "bedrock/us-gov-west-1/amazon.titan-embed-text-v2:0": { + "input_cost_per_token": 2e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 8192, + "max_tokens": 8192, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 1024 + }, + "bedrock/us-gov-west-1/amazon.titan-text-express-v1": { + "input_cost_per_token": 1.3e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 42000, + "max_output_tokens": 8000, + "max_tokens": 8000, + "mode": "chat", + "output_cost_per_token": 1.7e-06 + }, + "bedrock/us-gov-west-1/amazon.titan-text-lite-v1": { + "input_cost_per_token": 3e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 42000, + "max_output_tokens": 4000, + "max_tokens": 4000, + "mode": "chat", + "output_cost_per_token": 4e-07 + }, + "bedrock/us-gov-west-1/amazon.titan-text-premier-v1:0": { + "input_cost_per_token": 5e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 42000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 1.5e-06 + }, + "bedrock/us-gov-west-1/anthropic.claude-3-7-sonnet-20250219-v1:0": { + "cache_creation_input_token_cost": 4.5e-06, + "cache_read_input_token_cost": 3.6e-07, + "input_cost_per_token": 3.6e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.8e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "bedrock/us-gov-west-1/anthropic.claude-3-5-sonnet-20240620-v1:0": { + "input_cost_per_token": 3.6e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.8e-05, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "bedrock/us-gov-west-1/anthropic.claude-3-haiku-20240307-v1:0": { + "input_cost_per_token": 3e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "bedrock/us-gov-west-1/claude-sonnet-4-5-20250929-v1:0": { + "input_cost_per_token": 3.3e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.65e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "bedrock/us-gov-west-1/meta.llama3-70b-instruct-v1:0": { + "input_cost_per_token": 2.65e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 8000, + "max_output_tokens": 2048, + "max_tokens": 2048, + "mode": "chat", + "output_cost_per_token": 3.5e-06, + "supports_pdf_input": true + }, + "bedrock/us-gov-west-1/meta.llama3-8b-instruct-v1:0": { + "input_cost_per_token": 3e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 8000, + "max_output_tokens": 2048, + "max_tokens": 2048, + "mode": "chat", + "output_cost_per_token": 2.65e-06, + "supports_pdf_input": true + }, + "bedrock/us-west-1/meta.llama3-70b-instruct-v1:0": { + "input_cost_per_token": 2.65e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 3.5e-06 + }, + "bedrock/us-west-1/meta.llama3-8b-instruct-v1:0": { + "input_cost_per_token": 3e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 6e-07 + }, + "bedrock/us-west-2/1-month-commitment/anthropic.claude-instant-v1": { + "input_cost_per_second": 0.011, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_second": 0.011, + "supports_tool_choice": true + }, + "bedrock/us-west-2/1-month-commitment/anthropic.claude-v1": { + "input_cost_per_second": 0.0175, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_second": 0.0175 + }, + "bedrock/us-west-2/1-month-commitment/anthropic.claude-v2:1": { + "input_cost_per_second": 0.0175, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_second": 0.0175, + "supports_tool_choice": true + }, + "bedrock/us-west-2/6-month-commitment/anthropic.claude-instant-v1": { + "input_cost_per_second": 0.00611, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_second": 0.00611, + "supports_tool_choice": true + }, + "bedrock/us-west-2/6-month-commitment/anthropic.claude-v1": { + "input_cost_per_second": 0.00972, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_second": 0.00972 + }, + "bedrock/us-west-2/6-month-commitment/anthropic.claude-v2:1": { + "input_cost_per_second": 0.00972, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_second": 0.00972, + "supports_tool_choice": true + }, + "bedrock/us-west-2/anthropic.claude-instant-v1": { + "input_cost_per_token": 8e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2.4e-06, + "supports_tool_choice": true + }, + "bedrock/us-west-2/anthropic.claude-v1": { + "input_cost_per_token": 8e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2.4e-05, + "supports_tool_choice": true + }, + "bedrock/us-west-2/anthropic.claude-v2:1": { + "input_cost_per_token": 8e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 100000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2.4e-05, + "supports_tool_choice": true + }, + "bedrock/us-west-2/mistral.mistral-7b-instruct-v0:2": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2e-07, + "supports_tool_choice": true + }, + "bedrock/us-west-2/mistral.mistral-large-2402-v1:0": { + "input_cost_per_token": 8e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2.4e-05, + "supports_function_calling": true + }, + "bedrock/us-west-2/mistral.mixtral-8x7b-instruct-v0:1": { + "input_cost_per_token": 4.5e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 7e-07, + "supports_tool_choice": true + }, + "bedrock/us.anthropic.claude-3-5-haiku-20241022-v1:0": { + "cache_creation_input_token_cost": 1e-06, + "cache_read_input_token_cost": 8e-08, + "input_cost_per_token": 8e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 4e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "cerebras/llama-3.3-70b": { + "input_cost_per_token": 8.5e-07, + "litellm_provider": "cerebras", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "cerebras/llama3.1-70b": { + "input_cost_per_token": 6e-07, + "litellm_provider": "cerebras", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 6e-07, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "cerebras/llama3.1-8b": { + "input_cost_per_token": 1e-07, + "litellm_provider": "cerebras", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-07, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "cerebras/gpt-oss-120b": { + "input_cost_per_token": 2.5e-07, + "litellm_provider": "cerebras", + "max_input_tokens": 131072, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 6.9e-07, + "source": "https://www.cerebras.ai/blog/openai-gpt-oss-120b-runs-fastest-on-cerebras", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "cerebras/qwen-3-32b": { + "input_cost_per_token": 4e-07, + "litellm_provider": "cerebras", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 8e-07, + "source": "https://inference-docs.cerebras.ai/support/pricing", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "cerebras/zai-glm-4.6": { + "input_cost_per_token": 2.25e-06, + "litellm_provider": "cerebras", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 2.75e-06, + "source": "https://www.cerebras.ai/pricing", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "chat-bison": { + "input_cost_per_character": 2.5e-07, + "input_cost_per_token": 1.25e-07, + "litellm_provider": "vertex_ai-chat-models", + "max_input_tokens": 8192, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_character": 5e-07, + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_tool_choice": true + }, + "chat-bison-32k": { + "input_cost_per_character": 2.5e-07, + "input_cost_per_token": 1.25e-07, + "litellm_provider": "vertex_ai-chat-models", + "max_input_tokens": 32000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_character": 5e-07, + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_tool_choice": true + }, + "chat-bison-32k@002": { + "input_cost_per_character": 2.5e-07, + "input_cost_per_token": 1.25e-07, + "litellm_provider": "vertex_ai-chat-models", + "max_input_tokens": 32000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_character": 5e-07, + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_tool_choice": true + }, + "chat-bison@001": { + "input_cost_per_character": 2.5e-07, + "input_cost_per_token": 1.25e-07, + "litellm_provider": "vertex_ai-chat-models", + "max_input_tokens": 8192, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_character": 5e-07, + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_tool_choice": true + }, + "chat-bison@002": { + "deprecation_date": "2025-04-09", + "input_cost_per_character": 2.5e-07, + "input_cost_per_token": 1.25e-07, + "litellm_provider": "vertex_ai-chat-models", + "max_input_tokens": 8192, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_character": 5e-07, + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_tool_choice": true + }, + "chatdolphin": { + "input_cost_per_token": 5e-07, + "litellm_provider": "nlp_cloud", + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 5e-07 + }, + "chatgpt-4o-latest": { + "input_cost_per_token": 5e-06, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gpt-4o-transcribe-diarize": { + "input_cost_per_audio_token": 6e-06, + "input_cost_per_token": 2.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 16000, + "max_output_tokens": 2000, + "mode": "audio_transcription", + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "claude-3-5-haiku-20241022": { + "cache_creation_input_token_cost": 1e-06, + "cache_creation_input_token_cost_above_1hr": 6e-06, + "cache_read_input_token_cost": 8e-08, + "deprecation_date": "2025-10-01", + "input_cost_per_token": 8e-07, + "litellm_provider": "anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 4e-06, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true, + "tool_use_system_prompt_tokens": 264 + }, + "claude-3-5-haiku-latest": { + "cache_creation_input_token_cost": 1.25e-06, + "cache_creation_input_token_cost_above_1hr": 6e-06, + "cache_read_input_token_cost": 1e-07, + "deprecation_date": "2025-10-01", + "input_cost_per_token": 1e-06, + "litellm_provider": "anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 5e-06, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true, + "tool_use_system_prompt_tokens": 264 + }, + "claude-haiku-4-5-20251001": { + "cache_creation_input_token_cost": 1.25e-06, + "cache_creation_input_token_cost_above_1hr": 2e-06, + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 1e-06, + "litellm_provider": "anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 5e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_computer_use": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "claude-haiku-4-5": { + "cache_creation_input_token_cost": 1.25e-06, + "cache_creation_input_token_cost_above_1hr": 2e-06, + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 1e-06, + "litellm_provider": "anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 5e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_computer_use": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "claude-3-5-sonnet-20240620": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_creation_input_token_cost_above_1hr": 6e-06, + "cache_read_input_token_cost": 3e-07, + "deprecation_date": "2025-06-01", + "input_cost_per_token": 3e-06, + "litellm_provider": "anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "claude-3-5-sonnet-20241022": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_creation_input_token_cost_above_1hr": 6e-06, + "cache_read_input_token_cost": 3e-07, + "deprecation_date": "2025-10-01", + "input_cost_per_token": 3e-06, + "litellm_provider": "anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true, + "tool_use_system_prompt_tokens": 159 + }, + "claude-3-5-sonnet-latest": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_creation_input_token_cost_above_1hr": 6e-06, + "cache_read_input_token_cost": 3e-07, + "deprecation_date": "2025-06-01", + "input_cost_per_token": 3e-06, + "litellm_provider": "anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true, + "tool_use_system_prompt_tokens": 159 + }, + "claude-3-7-sonnet-20250219": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_creation_input_token_cost_above_1hr": 6e-06, + "cache_read_input_token_cost": 3e-07, + "deprecation_date": "2026-02-19", + "input_cost_per_token": 3e-06, + "litellm_provider": "anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true, + "tool_use_system_prompt_tokens": 159 + }, + "claude-3-7-sonnet-latest": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_creation_input_token_cost_above_1hr": 6e-06, + "cache_read_input_token_cost": 3e-07, + "deprecation_date": "2025-06-01", + "input_cost_per_token": 3e-06, + "litellm_provider": "anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "claude-3-haiku-20240307": { + "cache_creation_input_token_cost": 3e-07, + "cache_creation_input_token_cost_above_1hr": 6e-06, + "cache_read_input_token_cost": 3e-08, + "input_cost_per_token": 2.5e-07, + "litellm_provider": "anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.25e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 264 + }, + "claude-3-opus-20240229": { + "cache_creation_input_token_cost": 1.875e-05, + "cache_creation_input_token_cost_above_1hr": 6e-06, + "cache_read_input_token_cost": 1.5e-06, + "deprecation_date": "2026-05-01", + "input_cost_per_token": 1.5e-05, + "litellm_provider": "anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 395 + }, + "claude-3-opus-latest": { + "cache_creation_input_token_cost": 1.875e-05, + "cache_creation_input_token_cost_above_1hr": 6e-06, + "cache_read_input_token_cost": 1.5e-06, + "deprecation_date": "2025-03-01", + "input_cost_per_token": 1.5e-05, + "litellm_provider": "anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 395 + }, + "claude-4-opus-20250514": { + "cache_creation_input_token_cost": 1.875e-05, + "cache_read_input_token_cost": 1.5e-06, + "input_cost_per_token": 1.5e-05, + "litellm_provider": "anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "claude-4-sonnet-20250514": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost": 3e-07, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "litellm_provider": "anthropic", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 1000000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "claude-sonnet-4-5": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "litellm_provider": "anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 346 + }, + "claude-sonnet-4-5-20250929": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "litellm_provider": "anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true, + "tool_use_system_prompt_tokens": 346 + }, + "claude-sonnet-4-5-20250929-v1:0": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "claude-opus-4-1": { + "cache_creation_input_token_cost": 1.875e-05, + "cache_creation_input_token_cost_above_1hr": 3e-05, + "cache_read_input_token_cost": 1.5e-06, + "input_cost_per_token": 1.5e-05, + "litellm_provider": "anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "claude-opus-4-1-20250805": { + "cache_creation_input_token_cost": 1.875e-05, + "cache_creation_input_token_cost_above_1hr": 3e-05, + "cache_read_input_token_cost": 1.5e-06, + "input_cost_per_token": 1.5e-05, + "deprecation_date": "2026-08-05", + "litellm_provider": "anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "claude-opus-4-20250514": { + "cache_creation_input_token_cost": 1.875e-05, + "cache_creation_input_token_cost_above_1hr": 3e-05, + "cache_read_input_token_cost": 1.5e-06, + "input_cost_per_token": 1.5e-05, + "deprecation_date": "2026-05-14", + "litellm_provider": "anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "claude-opus-4-5-20251101": { + "cache_creation_input_token_cost": 6.25e-06, + "cache_creation_input_token_cost_above_1hr": 1e-05, + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 5e-06, + "litellm_provider": "anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "claude-opus-4-5": { + "cache_creation_input_token_cost": 6.25e-06, + "cache_creation_input_token_cost_above_1hr": 1e-05, + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 5e-06, + "litellm_provider": "anthropic", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "claude-sonnet-4-20250514": { + "deprecation_date": "2026-05-14", + "cache_creation_input_token_cost": 3.75e-06, + "cache_creation_input_token_cost_above_1hr": 6e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "litellm_provider": "anthropic", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "cloudflare/@cf/meta/llama-2-7b-chat-fp16": { + "input_cost_per_token": 1.923e-06, + "litellm_provider": "cloudflare", + "max_input_tokens": 3072, + "max_output_tokens": 3072, + "max_tokens": 3072, + "mode": "chat", + "output_cost_per_token": 1.923e-06 + }, + "cloudflare/@cf/meta/llama-2-7b-chat-int8": { + "input_cost_per_token": 1.923e-06, + "litellm_provider": "cloudflare", + "max_input_tokens": 2048, + "max_output_tokens": 2048, + "max_tokens": 2048, + "mode": "chat", + "output_cost_per_token": 1.923e-06 + }, + "cloudflare/@cf/mistral/mistral-7b-instruct-v0.1": { + "input_cost_per_token": 1.923e-06, + "litellm_provider": "cloudflare", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.923e-06 + }, + "cloudflare/@hf/thebloke/codellama-7b-instruct-awq": { + "input_cost_per_token": 1.923e-06, + "litellm_provider": "cloudflare", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.923e-06 + }, + "code-bison": { + "input_cost_per_character": 2.5e-07, + "input_cost_per_token": 1.25e-07, + "litellm_provider": "vertex_ai-code-text-models", + "max_input_tokens": 6144, + "max_output_tokens": 1024, + "max_tokens": 1024, + "mode": "chat", + "output_cost_per_character": 5e-07, + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_tool_choice": true + }, + "code-bison-32k@002": { + "input_cost_per_character": 2.5e-07, + "input_cost_per_token": 1.25e-07, + "litellm_provider": "vertex_ai-code-text-models", + "max_input_tokens": 6144, + "max_output_tokens": 1024, + "max_tokens": 1024, + "mode": "completion", + "output_cost_per_character": 5e-07, + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "code-bison32k": { + "input_cost_per_character": 2.5e-07, + "input_cost_per_token": 1.25e-07, + "litellm_provider": "vertex_ai-code-text-models", + "max_input_tokens": 6144, + "max_output_tokens": 1024, + "max_tokens": 1024, + "mode": "completion", + "output_cost_per_character": 5e-07, + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "code-bison@001": { + "input_cost_per_character": 2.5e-07, + "input_cost_per_token": 1.25e-07, + "litellm_provider": "vertex_ai-code-text-models", + "max_input_tokens": 6144, + "max_output_tokens": 1024, + "max_tokens": 1024, + "mode": "completion", + "output_cost_per_character": 5e-07, + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "code-bison@002": { + "input_cost_per_character": 2.5e-07, + "input_cost_per_token": 1.25e-07, + "litellm_provider": "vertex_ai-code-text-models", + "max_input_tokens": 6144, + "max_output_tokens": 1024, + "max_tokens": 1024, + "mode": "completion", + "output_cost_per_character": 5e-07, + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "code-gecko": { + "input_cost_per_token": 1.25e-07, + "litellm_provider": "vertex_ai-code-text-models", + "max_input_tokens": 2048, + "max_output_tokens": 64, + "max_tokens": 64, + "mode": "completion", + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "code-gecko-latest": { + "input_cost_per_token": 1.25e-07, + "litellm_provider": "vertex_ai-code-text-models", + "max_input_tokens": 2048, + "max_output_tokens": 64, + "max_tokens": 64, + "mode": "completion", + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "code-gecko@001": { + "input_cost_per_token": 1.25e-07, + "litellm_provider": "vertex_ai-code-text-models", + "max_input_tokens": 2048, + "max_output_tokens": 64, + "max_tokens": 64, + "mode": "completion", + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "code-gecko@002": { + "input_cost_per_token": 1.25e-07, + "litellm_provider": "vertex_ai-code-text-models", + "max_input_tokens": 2048, + "max_output_tokens": 64, + "max_tokens": 64, + "mode": "completion", + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "codechat-bison": { + "input_cost_per_character": 2.5e-07, + "input_cost_per_token": 1.25e-07, + "litellm_provider": "vertex_ai-code-chat-models", + "max_input_tokens": 6144, + "max_output_tokens": 1024, + "max_tokens": 1024, + "mode": "chat", + "output_cost_per_character": 5e-07, + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_tool_choice": true + }, + "codechat-bison-32k": { + "input_cost_per_character": 2.5e-07, + "input_cost_per_token": 1.25e-07, + "litellm_provider": "vertex_ai-code-chat-models", + "max_input_tokens": 32000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_character": 5e-07, + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_tool_choice": true + }, + "codechat-bison-32k@002": { + "input_cost_per_character": 2.5e-07, + "input_cost_per_token": 1.25e-07, + "litellm_provider": "vertex_ai-code-chat-models", + "max_input_tokens": 32000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_character": 5e-07, + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_tool_choice": true + }, + "codechat-bison@001": { + "input_cost_per_character": 2.5e-07, + "input_cost_per_token": 1.25e-07, + "litellm_provider": "vertex_ai-code-chat-models", + "max_input_tokens": 6144, + "max_output_tokens": 1024, + "max_tokens": 1024, + "mode": "chat", + "output_cost_per_character": 5e-07, + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_tool_choice": true + }, + "codechat-bison@002": { + "input_cost_per_character": 2.5e-07, + "input_cost_per_token": 1.25e-07, + "litellm_provider": "vertex_ai-code-chat-models", + "max_input_tokens": 6144, + "max_output_tokens": 1024, + "max_tokens": 1024, + "mode": "chat", + "output_cost_per_character": 5e-07, + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_tool_choice": true + }, + "codechat-bison@latest": { + "input_cost_per_character": 2.5e-07, + "input_cost_per_token": 1.25e-07, + "litellm_provider": "vertex_ai-code-chat-models", + "max_input_tokens": 6144, + "max_output_tokens": 1024, + "max_tokens": 1024, + "mode": "chat", + "output_cost_per_character": 5e-07, + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_tool_choice": true + }, + "codestral/codestral-2405": { + "input_cost_per_token": 0.0, + "litellm_provider": "codestral", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 0.0, + "source": "https://docs.mistral.ai/capabilities/code_generation/", + "supports_assistant_prefill": true, + "supports_tool_choice": true + }, + "codestral/codestral-latest": { + "input_cost_per_token": 0.0, + "litellm_provider": "codestral", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 0.0, + "source": "https://docs.mistral.ai/capabilities/code_generation/", + "supports_assistant_prefill": true, + "supports_tool_choice": true + }, + "codex-mini-latest": { + "cache_read_input_token_cost": 3.75e-07, + "input_cost_per_token": 1.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "responses", + "output_cost_per_token": 6e-06, + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "cohere.command-light-text-v14": { + "input_cost_per_token": 3e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 6e-07, + "supports_tool_choice": true + }, + "cohere.command-r-plus-v1:0": { + "input_cost_per_token": 3e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_tool_choice": true + }, + "cohere.command-r-v1:0": { + "input_cost_per_token": 5e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "supports_tool_choice": true + }, + "cohere.command-text-v14": { + "input_cost_per_token": 1.5e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 2e-06, + "supports_tool_choice": true + }, + "cohere.embed-english-v3": { + "input_cost_per_token": 1e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 512, + "max_tokens": 512, + "mode": "embedding", + "output_cost_per_token": 0.0, + "supports_embedding_image_input": true + }, + "cohere.embed-multilingual-v3": { + "input_cost_per_token": 1e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 512, + "max_tokens": 512, + "mode": "embedding", + "output_cost_per_token": 0.0, + "supports_embedding_image_input": true + }, + "cohere.embed-v4:0": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 128000, + "max_tokens": 128000, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 1536, + "supports_embedding_image_input": true + }, + "cohere/embed-v4.0": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "cohere", + "max_input_tokens": 128000, + "max_tokens": 128000, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 1536, + "supports_embedding_image_input": true + }, + "cohere.rerank-v3-5:0": { + "input_cost_per_query": 0.002, + "input_cost_per_token": 0.0, + "litellm_provider": "bedrock", + "max_document_chunks_per_query": 100, + "max_input_tokens": 32000, + "max_output_tokens": 32000, + "max_query_tokens": 32000, + "max_tokens": 32000, + "max_tokens_per_document_chunk": 512, + "mode": "rerank", + "output_cost_per_token": 0.0 + }, + "command": { + "input_cost_per_token": 1e-06, + "litellm_provider": "cohere", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "completion", + "output_cost_per_token": 2e-06 + }, + "command-a-03-2025": { + "input_cost_per_token": 2.5e-06, + "litellm_provider": "cohere_chat", + "max_input_tokens": 256000, + "max_output_tokens": 8000, + "max_tokens": 8000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "command-light": { + "input_cost_per_token": 3e-07, + "litellm_provider": "cohere_chat", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 6e-07, + "supports_tool_choice": true + }, + "command-nightly": { + "input_cost_per_token": 1e-06, + "litellm_provider": "cohere", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "completion", + "output_cost_per_token": 2e-06 + }, + "command-r": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "cohere_chat", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 6e-07, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "command-r-08-2024": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "cohere_chat", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 6e-07, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "command-r-plus": { + "input_cost_per_token": 2.5e-06, + "litellm_provider": "cohere_chat", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "command-r-plus-08-2024": { + "input_cost_per_token": 2.5e-06, + "litellm_provider": "cohere_chat", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "command-r7b-12-2024": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "cohere_chat", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 3.75e-08, + "source": "https://docs.cohere.com/v2/docs/command-r7b", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "computer-use-preview": { + "input_cost_per_token": 3e-06, + "litellm_provider": "azure", + "max_input_tokens": 8192, + "max_output_tokens": 1024, + "max_tokens": 1024, + "mode": "chat", + "output_cost_per_token": 1.2e-05, + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": false, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "deepseek-chat": { + "cache_read_input_token_cost": 6e-08, + "input_cost_per_token": 6e-07, + "litellm_provider": "deepseek", + "max_input_tokens": 131072, + "max_output_tokens": 8192, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1.7e-06, + "source": "https://api-docs.deepseek.com/quick_start/pricing", + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "deepseek-reasoner": { + "cache_read_input_token_cost": 6e-08, + "input_cost_per_token": 6e-07, + "litellm_provider": "deepseek", + "max_input_tokens": 131072, + "max_output_tokens": 65536, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1.7e-06, + "source": "https://api-docs.deepseek.com/quick_start/pricing", + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supports_function_calling": false, + "supports_native_streaming": true, + "supports_parallel_function_calling": false, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": false + }, + "dashscope/qwen-coder": { + "input_cost_per_token": 3e-07, + "litellm_provider": "dashscope", + "max_input_tokens": 1000000, + "max_output_tokens": 16384, + "max_tokens": 1000000, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "dashscope/qwen-flash": { + "litellm_provider": "dashscope", + "max_input_tokens": 997952, + "max_output_tokens": 32768, + "max_tokens": 1000000, + "mode": "chat", + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "tiered_pricing": [ + { + "input_cost_per_token": 5e-08, + "output_cost_per_token": 4e-07, + "range": [ + 0, + 256000.0 + ] + }, + { + "input_cost_per_token": 2.5e-07, + "output_cost_per_token": 2e-06, + "range": [ + 256000.0, + 1000000.0 + ] + } + ] + }, + "dashscope/qwen-flash-2025-07-28": { + "litellm_provider": "dashscope", + "max_input_tokens": 997952, + "max_output_tokens": 32768, + "max_tokens": 1000000, + "mode": "chat", + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "tiered_pricing": [ + { + "input_cost_per_token": 5e-08, + "output_cost_per_token": 4e-07, + "range": [ + 0, + 256000.0 + ] + }, + { + "input_cost_per_token": 2.5e-07, + "output_cost_per_token": 2e-06, + "range": [ + 256000.0, + 1000000.0 + ] + } + ] + }, + "dashscope/qwen-max": { + "input_cost_per_token": 1.6e-06, + "litellm_provider": "dashscope", + "max_input_tokens": 30720, + "max_output_tokens": 8192, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 6.4e-06, + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "dashscope/qwen-plus": { + "input_cost_per_token": 4e-07, + "litellm_provider": "dashscope", + "max_input_tokens": 129024, + "max_output_tokens": 16384, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "dashscope/qwen-plus-2025-01-25": { + "input_cost_per_token": 4e-07, + "litellm_provider": "dashscope", + "max_input_tokens": 129024, + "max_output_tokens": 8192, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "dashscope/qwen-plus-2025-04-28": { + "input_cost_per_token": 4e-07, + "litellm_provider": "dashscope", + "max_input_tokens": 129024, + "max_output_tokens": 16384, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_reasoning_token": 4e-06, + "output_cost_per_token": 1.2e-06, + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "dashscope/qwen-plus-2025-07-14": { + "input_cost_per_token": 4e-07, + "litellm_provider": "dashscope", + "max_input_tokens": 129024, + "max_output_tokens": 16384, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_reasoning_token": 4e-06, + "output_cost_per_token": 1.2e-06, + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "dashscope/qwen-plus-2025-07-28": { + "litellm_provider": "dashscope", + "max_input_tokens": 997952, + "max_output_tokens": 32768, + "max_tokens": 1000000, + "mode": "chat", + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "tiered_pricing": [ + { + "input_cost_per_token": 4e-07, + "output_cost_per_reasoning_token": 4e-06, + "output_cost_per_token": 1.2e-06, + "range": [ + 0, + 256000.0 + ] + }, + { + "input_cost_per_token": 1.2e-06, + "output_cost_per_reasoning_token": 1.2e-05, + "output_cost_per_token": 3.6e-06, + "range": [ + 256000.0, + 1000000.0 + ] + } + ] + }, + "dashscope/qwen-plus-2025-09-11": { + "litellm_provider": "dashscope", + "max_input_tokens": 997952, + "max_output_tokens": 32768, + "max_tokens": 1000000, + "mode": "chat", + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "tiered_pricing": [ + { + "input_cost_per_token": 4e-07, + "output_cost_per_reasoning_token": 4e-06, + "output_cost_per_token": 1.2e-06, + "range": [ + 0, + 256000.0 + ] + }, + { + "input_cost_per_token": 1.2e-06, + "output_cost_per_reasoning_token": 1.2e-05, + "output_cost_per_token": 3.6e-06, + "range": [ + 256000.0, + 1000000.0 + ] + } + ] + }, + "dashscope/qwen-plus-latest": { + "litellm_provider": "dashscope", + "max_input_tokens": 997952, + "max_output_tokens": 32768, + "max_tokens": 1000000, + "mode": "chat", + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "tiered_pricing": [ + { + "input_cost_per_token": 4e-07, + "output_cost_per_reasoning_token": 4e-06, + "output_cost_per_token": 1.2e-06, + "range": [ + 0, + 256000.0 + ] + }, + { + "input_cost_per_token": 1.2e-06, + "output_cost_per_reasoning_token": 1.2e-05, + "output_cost_per_token": 3.6e-06, + "range": [ + 256000.0, + 1000000.0 + ] + } + ] + }, + "dashscope/qwen-turbo": { + "input_cost_per_token": 5e-08, + "litellm_provider": "dashscope", + "max_input_tokens": 129024, + "max_output_tokens": 16384, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_reasoning_token": 5e-07, + "output_cost_per_token": 2e-07, + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "dashscope/qwen-turbo-2024-11-01": { + "input_cost_per_token": 5e-08, + "litellm_provider": "dashscope", + "max_input_tokens": 1000000, + "max_output_tokens": 8192, + "max_tokens": 1000000, + "mode": "chat", + "output_cost_per_token": 2e-07, + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "dashscope/qwen-turbo-2025-04-28": { + "input_cost_per_token": 5e-08, + "litellm_provider": "dashscope", + "max_input_tokens": 1000000, + "max_output_tokens": 16384, + "max_tokens": 1000000, + "mode": "chat", + "output_cost_per_reasoning_token": 5e-07, + "output_cost_per_token": 2e-07, + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "dashscope/qwen-turbo-latest": { + "input_cost_per_token": 5e-08, + "litellm_provider": "dashscope", + "max_input_tokens": 1000000, + "max_output_tokens": 16384, + "max_tokens": 1000000, + "mode": "chat", + "output_cost_per_reasoning_token": 5e-07, + "output_cost_per_token": 2e-07, + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "dashscope/qwen3-30b-a3b": { + "litellm_provider": "dashscope", + "max_input_tokens": 129024, + "max_output_tokens": 16384, + "max_tokens": 131072, + "mode": "chat", + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "dashscope/qwen3-coder-flash": { + "litellm_provider": "dashscope", + "max_input_tokens": 997952, + "max_output_tokens": 65536, + "max_tokens": 1000000, + "mode": "chat", + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "tiered_pricing": [ + { + "cache_read_input_token_cost": 8e-08, + "input_cost_per_token": 3e-07, + "output_cost_per_token": 1.5e-06, + "range": [ + 0, + 32000.0 + ] + }, + { + "cache_read_input_token_cost": 1.2e-07, + "input_cost_per_token": 5e-07, + "output_cost_per_token": 2.5e-06, + "range": [ + 32000.0, + 128000.0 + ] + }, + { + "cache_read_input_token_cost": 2e-07, + "input_cost_per_token": 8e-07, + "output_cost_per_token": 4e-06, + "range": [ + 128000.0, + 256000.0 + ] + }, + { + "cache_read_input_token_cost": 4e-07, + "input_cost_per_token": 1.6e-06, + "output_cost_per_token": 9.6e-06, + "range": [ + 256000.0, + 1000000.0 + ] + } + ] + }, + "dashscope/qwen3-coder-flash-2025-07-28": { + "litellm_provider": "dashscope", + "max_input_tokens": 997952, + "max_output_tokens": 65536, + "max_tokens": 1000000, + "mode": "chat", + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "tiered_pricing": [ + { + "input_cost_per_token": 3e-07, + "output_cost_per_token": 1.5e-06, + "range": [ + 0, + 32000.0 + ] + }, + { + "input_cost_per_token": 5e-07, + "output_cost_per_token": 2.5e-06, + "range": [ + 32000.0, + 128000.0 + ] + }, + { + "input_cost_per_token": 8e-07, + "output_cost_per_token": 4e-06, + "range": [ + 128000.0, + 256000.0 + ] + }, + { + "input_cost_per_token": 1.6e-06, + "output_cost_per_token": 9.6e-06, + "range": [ + 256000.0, + 1000000.0 + ] + } + ] + }, + "dashscope/qwen3-coder-plus": { + "litellm_provider": "dashscope", + "max_input_tokens": 997952, + "max_output_tokens": 65536, + "max_tokens": 1000000, + "mode": "chat", + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "tiered_pricing": [ + { + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 1e-06, + "output_cost_per_token": 5e-06, + "range": [ + 0, + 32000.0 + ] + }, + { + "cache_read_input_token_cost": 1.8e-07, + "input_cost_per_token": 1.8e-06, + "output_cost_per_token": 9e-06, + "range": [ + 32000.0, + 128000.0 + ] + }, + { + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "output_cost_per_token": 1.5e-05, + "range": [ + 128000.0, + 256000.0 + ] + }, + { + "cache_read_input_token_cost": 6e-07, + "input_cost_per_token": 6e-06, + "output_cost_per_token": 6e-05, + "range": [ + 256000.0, + 1000000.0 + ] + } + ] + }, + "dashscope/qwen3-coder-plus-2025-07-22": { + "litellm_provider": "dashscope", + "max_input_tokens": 997952, + "max_output_tokens": 65536, + "max_tokens": 1000000, + "mode": "chat", + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "tiered_pricing": [ + { + "input_cost_per_token": 1e-06, + "output_cost_per_token": 5e-06, + "range": [ + 0, + 32000.0 + ] + }, + { + "input_cost_per_token": 1.8e-06, + "output_cost_per_token": 9e-06, + "range": [ + 32000.0, + 128000.0 + ] + }, + { + "input_cost_per_token": 3e-06, + "output_cost_per_token": 1.5e-05, + "range": [ + 128000.0, + 256000.0 + ] + }, + { + "input_cost_per_token": 6e-06, + "output_cost_per_token": 6e-05, + "range": [ + 256000.0, + 1000000.0 + ] + } + ] + }, + "dashscope/qwen3-max-preview": { + "litellm_provider": "dashscope", + "max_input_tokens": 258048, + "max_output_tokens": 65536, + "max_tokens": 262144, + "mode": "chat", + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "tiered_pricing": [ + { + "input_cost_per_token": 1.2e-06, + "output_cost_per_token": 6e-06, + "range": [ + 0, + 32000.0 + ] + }, + { + "input_cost_per_token": 2.4e-06, + "output_cost_per_token": 1.2e-05, + "range": [ + 32000.0, + 128000.0 + ] + }, + { + "input_cost_per_token": 3e-06, + "output_cost_per_token": 1.5e-05, + "range": [ + 128000.0, + 252000.0 + ] + } + ] + }, + "dashscope/qwq-plus": { + "input_cost_per_token": 8e-07, + "litellm_provider": "dashscope", + "max_input_tokens": 98304, + "max_output_tokens": 8192, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2.4e-06, + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-bge-large-en": { + "input_cost_per_token": 1.0003e-07, + "input_dbu_cost_per_token": 1.429e-06, + "litellm_provider": "databricks", + "max_input_tokens": 512, + "max_tokens": 512, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_dbu_cost_per_token": 0.0, + "output_vector_size": 1024, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving" + }, + "databricks/databricks-claude-3-7-sonnet": { + "input_cost_per_token": 2.9999900000000002e-06, + "input_dbu_cost_per_token": 4.2857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 128000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 1.5000020000000002e-05, + "output_dbu_cost_per_token": 0.000214286, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-haiku-4-5": { + "input_cost_per_token": 1.00002e-06, + "input_dbu_cost_per_token": 1.4286e-05, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 5.00003e-06, + "output_dbu_cost_per_token": 7.1429e-05, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-opus-4": { + "input_cost_per_token": 1.5000020000000002e-05, + "input_dbu_cost_per_token": 0.000214286, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 7.500003000000001e-05, + "output_dbu_cost_per_token": 0.001071429, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-opus-4-1": { + "input_cost_per_token": 1.5000020000000002e-05, + "input_dbu_cost_per_token": 0.000214286, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 7.500003000000001e-05, + "output_dbu_cost_per_token": 0.001071429, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-opus-4-5": { + "input_cost_per_token": 5.00003e-06, + "input_dbu_cost_per_token": 7.1429e-05, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 2.5000010000000002e-05, + "output_dbu_cost_per_token": 0.000357143, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-sonnet-4": { + "input_cost_per_token": 2.9999900000000002e-06, + "input_dbu_cost_per_token": 4.2857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 1.5000020000000002e-05, + "output_dbu_cost_per_token": 0.000214286, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-sonnet-4-1": { + "input_cost_per_token": 2.9999900000000002e-06, + "input_dbu_cost_per_token": 4.2857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 1.5000020000000002e-05, + "output_dbu_cost_per_token": 0.000214286, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-claude-sonnet-4-5": { + "input_cost_per_token": 2.9999900000000002e-06, + "input_dbu_cost_per_token": 4.2857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 1.5000020000000002e-05, + "output_dbu_cost_per_token": 0.000214286, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "databricks/databricks-gemini-2-5-flash": { + "input_cost_per_token": 3.0001999999999996e-07, + "input_dbu_cost_per_token": 4.285999999999999e-06, + "litellm_provider": "databricks", + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_tokens": 1048576, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 2.49998e-06, + "output_dbu_cost_per_token": 3.5714e-05, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "databricks/databricks-gemini-2-5-pro": { + "input_cost_per_token": 1.24999e-06, + "input_dbu_cost_per_token": 1.7857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 1048576, + "max_output_tokens": 65536, + "max_tokens": 1048576, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 9.999990000000002e-06, + "output_dbu_cost_per_token": 0.000142857, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "databricks/databricks-gemma-3-12b": { + "input_cost_per_token": 1.5000999999999998e-07, + "input_dbu_cost_per_token": 2.1429999999999996e-06, + "litellm_provider": "databricks", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "max_tokens": 128000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 5.0001e-07, + "output_dbu_cost_per_token": 7.143e-06, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving" + }, + "databricks/databricks-gpt-5": { + "input_cost_per_token": 1.24999e-06, + "input_dbu_cost_per_token": 1.7857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 400000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 9.999990000000002e-06, + "output_dbu_cost_per_token": 0.000142857, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving" + }, + "databricks/databricks-gpt-5-1": { + "input_cost_per_token": 1.24999e-06, + "input_dbu_cost_per_token": 1.7857e-05, + "litellm_provider": "databricks", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 400000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 9.999990000000002e-06, + "output_dbu_cost_per_token": 0.000142857, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving" + }, + "databricks/databricks-gpt-5-mini": { + "input_cost_per_token": 2.4997000000000006e-07, + "input_dbu_cost_per_token": 3.571e-06, + "litellm_provider": "databricks", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 400000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 1.9999700000000004e-06, + "output_dbu_cost_per_token": 2.8571e-05, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving" + }, + "databricks/databricks-gpt-5-nano": { + "input_cost_per_token": 4.998e-08, + "input_dbu_cost_per_token": 7.14e-07, + "litellm_provider": "databricks", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 400000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 3.9998000000000007e-07, + "output_dbu_cost_per_token": 5.714000000000001e-06, + "source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving" + }, + "databricks/databricks-gpt-oss-120b": { + "input_cost_per_token": 1.5000999999999998e-07, + "input_dbu_cost_per_token": 2.1429999999999996e-06, + "litellm_provider": "databricks", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 5.9997e-07, + "output_dbu_cost_per_token": 8.571e-06, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving" + }, + "databricks/databricks-gpt-oss-20b": { + "input_cost_per_token": 7e-08, + "input_dbu_cost_per_token": 1e-06, + "litellm_provider": "databricks", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 3.0001999999999996e-07, + "output_dbu_cost_per_token": 4.285999999999999e-06, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving" + }, + "databricks/databricks-gte-large-en": { + "input_cost_per_token": 1.2999000000000001e-07, + "input_dbu_cost_per_token": 1.857e-06, + "litellm_provider": "databricks", + "max_input_tokens": 8192, + "max_tokens": 8192, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_dbu_cost_per_token": 0.0, + "output_vector_size": 1024, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving" + }, + "databricks/databricks-llama-2-70b-chat": { + "input_cost_per_token": 5.0001e-07, + "input_dbu_cost_per_token": 7.143e-06, + "litellm_provider": "databricks", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 1.5000300000000002e-06, + "output_dbu_cost_per_token": 2.1429e-05, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving", + "supports_tool_choice": true + }, + "databricks/databricks-llama-4-maverick": { + "input_cost_per_token": 5.0001e-07, + "input_dbu_cost_per_token": 7.143e-06, + "litellm_provider": "databricks", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "metadata": { + "notes": "Databricks documentation now provides both DBU costs (_dbu_cost_per_token) and dollar costs(_cost_per_token)." + }, + "mode": "chat", + "output_cost_per_token": 1.5000300000000002e-06, + "output_dbu_cost_per_token": 2.1429e-05, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving", + "supports_tool_choice": true + }, + "databricks/databricks-meta-llama-3-1-405b-instruct": { + "input_cost_per_token": 5.00003e-06, + "input_dbu_cost_per_token": 7.1429e-05, + "litellm_provider": "databricks", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 1.5000020000000002e-05, + "output_dbu_cost_per_token": 0.000214286, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving", + "supports_tool_choice": true + }, + "databricks/databricks-meta-llama-3-1-8b-instruct": { + "input_cost_per_token": 1.5000999999999998e-07, + "input_dbu_cost_per_token": 2.1429999999999996e-06, + "litellm_provider": "databricks", + "max_input_tokens": 200000, + "max_output_tokens": 128000, + "max_tokens": 200000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 4.5003000000000007e-07, + "output_dbu_cost_per_token": 6.429000000000001e-06, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving" + }, + "databricks/databricks-meta-llama-3-3-70b-instruct": { + "input_cost_per_token": 5.0001e-07, + "input_dbu_cost_per_token": 7.143e-06, + "litellm_provider": "databricks", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 1.5000300000000002e-06, + "output_dbu_cost_per_token": 2.1429e-05, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving", + "supports_tool_choice": true + }, + "databricks/databricks-meta-llama-3-70b-instruct": { + "input_cost_per_token": 1.00002e-06, + "input_dbu_cost_per_token": 1.4286e-05, + "litellm_provider": "databricks", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 2.9999900000000002e-06, + "output_dbu_cost_per_token": 4.2857e-05, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving", + "supports_tool_choice": true + }, + "databricks/databricks-mixtral-8x7b-instruct": { + "input_cost_per_token": 5.0001e-07, + "input_dbu_cost_per_token": 7.143e-06, + "litellm_provider": "databricks", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 1.00002e-06, + "output_dbu_cost_per_token": 1.4286e-05, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving", + "supports_tool_choice": true + }, + "databricks/databricks-mpt-30b-instruct": { + "input_cost_per_token": 1.00002e-06, + "input_dbu_cost_per_token": 1.4286e-05, + "litellm_provider": "databricks", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 1.00002e-06, + "output_dbu_cost_per_token": 1.4286e-05, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving", + "supports_tool_choice": true + }, + "databricks/databricks-mpt-7b-instruct": { + "input_cost_per_token": 5.0001e-07, + "input_dbu_cost_per_token": 7.143e-06, + "litellm_provider": "databricks", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "metadata": { + "notes": "Input/output cost per token is dbu cost * $0.070, based on databricks Llama 3.1 70B conversion. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation." + }, + "mode": "chat", + "output_cost_per_token": 0.0, + "output_dbu_cost_per_token": 0.0, + "source": "https://www.databricks.com/product/pricing/foundation-model-serving", + "supports_tool_choice": true + }, + "dataforseo/search": { + "input_cost_per_query": 0.003, + "litellm_provider": "dataforseo", + "mode": "search" + }, + "davinci-002": { + "input_cost_per_token": 2e-06, + "litellm_provider": "text-completion-openai", + "max_input_tokens": 16384, + "max_output_tokens": 4096, + "max_tokens": 16384, + "mode": "completion", + "output_cost_per_token": 2e-06 + }, + "deepgram/base": { + "input_cost_per_second": 0.00020833, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0125/60 seconds = $0.00020833 per second", + "original_pricing_per_minute": 0.0125 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/base-conversationalai": { + "input_cost_per_second": 0.00020833, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0125/60 seconds = $0.00020833 per second", + "original_pricing_per_minute": 0.0125 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/base-finance": { + "input_cost_per_second": 0.00020833, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0125/60 seconds = $0.00020833 per second", + "original_pricing_per_minute": 0.0125 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/base-general": { + "input_cost_per_second": 0.00020833, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0125/60 seconds = $0.00020833 per second", + "original_pricing_per_minute": 0.0125 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/base-meeting": { + "input_cost_per_second": 0.00020833, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0125/60 seconds = $0.00020833 per second", + "original_pricing_per_minute": 0.0125 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/base-phonecall": { + "input_cost_per_second": 0.00020833, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0125/60 seconds = $0.00020833 per second", + "original_pricing_per_minute": 0.0125 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/base-video": { + "input_cost_per_second": 0.00020833, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0125/60 seconds = $0.00020833 per second", + "original_pricing_per_minute": 0.0125 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/base-voicemail": { + "input_cost_per_second": 0.00020833, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0125/60 seconds = $0.00020833 per second", + "original_pricing_per_minute": 0.0125 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/enhanced": { + "input_cost_per_second": 0.00024167, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0145/60 seconds = $0.00024167 per second", + "original_pricing_per_minute": 0.0145 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/enhanced-finance": { + "input_cost_per_second": 0.00024167, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0145/60 seconds = $0.00024167 per second", + "original_pricing_per_minute": 0.0145 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/enhanced-general": { + "input_cost_per_second": 0.00024167, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0145/60 seconds = $0.00024167 per second", + "original_pricing_per_minute": 0.0145 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/enhanced-meeting": { + "input_cost_per_second": 0.00024167, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0145/60 seconds = $0.00024167 per second", + "original_pricing_per_minute": 0.0145 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/enhanced-phonecall": { + "input_cost_per_second": 0.00024167, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0145/60 seconds = $0.00024167 per second", + "original_pricing_per_minute": 0.0145 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/nova": { + "input_cost_per_second": 7.167e-05, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0043/60 seconds = $0.00007167 per second", + "original_pricing_per_minute": 0.0043 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/nova-2": { + "input_cost_per_second": 7.167e-05, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0043/60 seconds = $0.00007167 per second", + "original_pricing_per_minute": 0.0043 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/nova-2-atc": { + "input_cost_per_second": 7.167e-05, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0043/60 seconds = $0.00007167 per second", + "original_pricing_per_minute": 0.0043 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/nova-2-automotive": { + "input_cost_per_second": 7.167e-05, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0043/60 seconds = $0.00007167 per second", + "original_pricing_per_minute": 0.0043 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/nova-2-conversationalai": { + "input_cost_per_second": 7.167e-05, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0043/60 seconds = $0.00007167 per second", + "original_pricing_per_minute": 0.0043 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/nova-2-drivethru": { + "input_cost_per_second": 7.167e-05, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0043/60 seconds = $0.00007167 per second", + "original_pricing_per_minute": 0.0043 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/nova-2-finance": { + "input_cost_per_second": 7.167e-05, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0043/60 seconds = $0.00007167 per second", + "original_pricing_per_minute": 0.0043 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/nova-2-general": { + "input_cost_per_second": 7.167e-05, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0043/60 seconds = $0.00007167 per second", + "original_pricing_per_minute": 0.0043 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/nova-2-meeting": { + "input_cost_per_second": 7.167e-05, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0043/60 seconds = $0.00007167 per second", + "original_pricing_per_minute": 0.0043 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/nova-2-phonecall": { + "input_cost_per_second": 7.167e-05, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0043/60 seconds = $0.00007167 per second", + "original_pricing_per_minute": 0.0043 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/nova-2-video": { + "input_cost_per_second": 7.167e-05, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0043/60 seconds = $0.00007167 per second", + "original_pricing_per_minute": 0.0043 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/nova-2-voicemail": { + "input_cost_per_second": 7.167e-05, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0043/60 seconds = $0.00007167 per second", + "original_pricing_per_minute": 0.0043 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/nova-3": { + "input_cost_per_second": 7.167e-05, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0043/60 seconds = $0.00007167 per second", + "original_pricing_per_minute": 0.0043 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/nova-3-general": { + "input_cost_per_second": 7.167e-05, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0043/60 seconds = $0.00007167 per second", + "original_pricing_per_minute": 0.0043 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/nova-3-medical": { + "input_cost_per_second": 8.667e-05, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0052/60 seconds = $0.00008667 per second (multilingual)", + "original_pricing_per_minute": 0.0052 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/nova-general": { + "input_cost_per_second": 7.167e-05, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0043/60 seconds = $0.00007167 per second", + "original_pricing_per_minute": 0.0043 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/nova-phonecall": { + "input_cost_per_second": 7.167e-05, + "litellm_provider": "deepgram", + "metadata": { + "calculation": "$0.0043/60 seconds = $0.00007167 per second", + "original_pricing_per_minute": 0.0043 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/whisper": { + "input_cost_per_second": 0.0001, + "litellm_provider": "deepgram", + "metadata": { + "notes": "Deepgram's hosted OpenAI Whisper models - pricing may differ from native Deepgram models" + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/whisper-base": { + "input_cost_per_second": 0.0001, + "litellm_provider": "deepgram", + "metadata": { + "notes": "Deepgram's hosted OpenAI Whisper models - pricing may differ from native Deepgram models" + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/whisper-large": { + "input_cost_per_second": 0.0001, + "litellm_provider": "deepgram", + "metadata": { + "notes": "Deepgram's hosted OpenAI Whisper models - pricing may differ from native Deepgram models" + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/whisper-medium": { + "input_cost_per_second": 0.0001, + "litellm_provider": "deepgram", + "metadata": { + "notes": "Deepgram's hosted OpenAI Whisper models - pricing may differ from native Deepgram models" + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/whisper-small": { + "input_cost_per_second": 0.0001, + "litellm_provider": "deepgram", + "metadata": { + "notes": "Deepgram's hosted OpenAI Whisper models - pricing may differ from native Deepgram models" + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepgram/whisper-tiny": { + "input_cost_per_second": 0.0001, + "litellm_provider": "deepgram", + "metadata": { + "notes": "Deepgram's hosted OpenAI Whisper models - pricing may differ from native Deepgram models" + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://deepgram.com/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "deepinfra/Gryphe/MythoMax-L2-13b": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 8e-08, + "output_cost_per_token": 9e-08, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/NousResearch/Hermes-3-Llama-3.1-405B": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 1e-06, + "output_cost_per_token": 1e-06, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/NousResearch/Hermes-3-Llama-3.1-70B": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 3e-07, + "output_cost_per_token": 3e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": false + }, + "deepinfra/Qwen/QwQ-32B": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 1.5e-07, + "output_cost_per_token": 4e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/Qwen/Qwen2.5-72B-Instruct": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 1.2e-07, + "output_cost_per_token": 3.9e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/Qwen/Qwen2.5-7B-Instruct": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 4e-08, + "output_cost_per_token": 1e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": false + }, + "deepinfra/Qwen/Qwen2.5-VL-32B-Instruct": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 6e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true, + "supports_vision": true + }, + "deepinfra/Qwen/Qwen3-14B": { + "max_tokens": 40960, + "max_input_tokens": 40960, + "max_output_tokens": 40960, + "input_cost_per_token": 6e-08, + "output_cost_per_token": 2.4e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/Qwen/Qwen3-235B-A22B": { + "max_tokens": 40960, + "max_input_tokens": 40960, + "max_output_tokens": 40960, + "input_cost_per_token": 1.8e-07, + "output_cost_per_token": 5.4e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/Qwen/Qwen3-235B-A22B-Instruct-2507": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 9e-08, + "output_cost_per_token": 6e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/Qwen/Qwen3-235B-A22B-Thinking-2507": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 3e-07, + "output_cost_per_token": 2.9e-06, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/Qwen/Qwen3-30B-A3B": { + "max_tokens": 40960, + "max_input_tokens": 40960, + "max_output_tokens": 40960, + "input_cost_per_token": 8e-08, + "output_cost_per_token": 2.9e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/Qwen/Qwen3-32B": { + "max_tokens": 40960, + "max_input_tokens": 40960, + "max_output_tokens": 40960, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 2.8e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/Qwen/Qwen3-Coder-480B-A35B-Instruct": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 4e-07, + "output_cost_per_token": 1.6e-06, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/Qwen/Qwen3-Coder-480B-A35B-Instruct-Turbo": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 2.9e-07, + "output_cost_per_token": 1.2e-06, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/Qwen/Qwen3-Next-80B-A3B-Instruct": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 1.4e-07, + "output_cost_per_token": 1.4e-06, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/Qwen/Qwen3-Next-80B-A3B-Thinking": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 1.4e-07, + "output_cost_per_token": 1.4e-06, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/Sao10K/L3-8B-Lunaris-v1-Turbo": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 4e-08, + "output_cost_per_token": 5e-08, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": false + }, + "deepinfra/Sao10K/L3.1-70B-Euryale-v2.2": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 6.5e-07, + "output_cost_per_token": 7.5e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": false + }, + "deepinfra/Sao10K/L3.3-70B-Euryale-v2.3": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 6.5e-07, + "output_cost_per_token": 7.5e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": false + }, + "deepinfra/allenai/olmOCR-7B-0725-FP8": { + "max_tokens": 16384, + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "input_cost_per_token": 2.7e-07, + "output_cost_per_token": 1.5e-06, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": false + }, + "deepinfra/anthropic/claude-3-7-sonnet-latest": { + "max_tokens": 200000, + "max_input_tokens": 200000, + "max_output_tokens": 200000, + "input_cost_per_token": 3.3e-06, + "output_cost_per_token": 1.65e-05, + "cache_read_input_token_cost": 3.3e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/anthropic/claude-4-opus": { + "max_tokens": 200000, + "max_input_tokens": 200000, + "max_output_tokens": 200000, + "input_cost_per_token": 1.65e-05, + "output_cost_per_token": 8.25e-05, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/anthropic/claude-4-sonnet": { + "max_tokens": 200000, + "max_input_tokens": 200000, + "max_output_tokens": 200000, + "input_cost_per_token": 3.3e-06, + "output_cost_per_token": 1.65e-05, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/deepseek-ai/DeepSeek-R1": { + "max_tokens": 163840, + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "input_cost_per_token": 7e-07, + "output_cost_per_token": 2.4e-06, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/deepseek-ai/DeepSeek-R1-0528": { + "max_tokens": 163840, + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "input_cost_per_token": 5e-07, + "output_cost_per_token": 2.15e-06, + "cache_read_input_token_cost": 4e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/deepseek-ai/DeepSeek-R1-0528-Turbo": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 1e-06, + "output_cost_per_token": 3e-06, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/deepseek-ai/DeepSeek-R1-Distill-Llama-70B": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 6e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": false + }, + "deepinfra/deepseek-ai/DeepSeek-R1-Distill-Qwen-32B": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 2.7e-07, + "output_cost_per_token": 2.7e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/deepseek-ai/DeepSeek-R1-Turbo": { + "max_tokens": 40960, + "max_input_tokens": 40960, + "max_output_tokens": 40960, + "input_cost_per_token": 1e-06, + "output_cost_per_token": 3e-06, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/deepseek-ai/DeepSeek-V3": { + "max_tokens": 163840, + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "input_cost_per_token": 3.8e-07, + "output_cost_per_token": 8.9e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/deepseek-ai/DeepSeek-V3-0324": { + "max_tokens": 163840, + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "input_cost_per_token": 2.5e-07, + "output_cost_per_token": 8.8e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/deepseek-ai/DeepSeek-V3.1": { + "max_tokens": 163840, + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "input_cost_per_token": 2.7e-07, + "output_cost_per_token": 1e-06, + "cache_read_input_token_cost": 2.16e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true, + "supports_reasoning": true + }, + "deepinfra/deepseek-ai/DeepSeek-V3.1-Terminus": { + "max_tokens": 163840, + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "input_cost_per_token": 2.7e-07, + "output_cost_per_token": 1e-06, + "cache_read_input_token_cost": 2.16e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/google/gemini-2.0-flash-001": { + "max_tokens": 1000000, + "max_input_tokens": 1000000, + "max_output_tokens": 1000000, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 4e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/google/gemini-2.5-flash": { + "max_tokens": 1000000, + "max_input_tokens": 1000000, + "max_output_tokens": 1000000, + "input_cost_per_token": 3e-07, + "output_cost_per_token": 2.5e-06, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/google/gemini-2.5-pro": { + "max_tokens": 1000000, + "max_input_tokens": 1000000, + "max_output_tokens": 1000000, + "input_cost_per_token": 1.25e-06, + "output_cost_per_token": 1e-05, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/google/gemma-3-12b-it": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 5e-08, + "output_cost_per_token": 1e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/google/gemma-3-27b-it": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 9e-08, + "output_cost_per_token": 1.6e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/google/gemma-3-4b-it": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 4e-08, + "output_cost_per_token": 8e-08, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/meta-llama/Llama-3.2-11B-Vision-Instruct": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 4.9e-08, + "output_cost_per_token": 4.9e-08, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": false + }, + "deepinfra/meta-llama/Llama-3.2-3B-Instruct": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 2e-08, + "output_cost_per_token": 2e-08, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/meta-llama/Llama-3.3-70B-Instruct": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 2.3e-07, + "output_cost_per_token": 4e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/meta-llama/Llama-3.3-70B-Instruct-Turbo": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 1.3e-07, + "output_cost_per_token": 3.9e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": { + "max_tokens": 1048576, + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "input_cost_per_token": 1.5e-07, + "output_cost_per_token": 6e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/meta-llama/Llama-4-Scout-17B-16E-Instruct": { + "max_tokens": 327680, + "max_input_tokens": 327680, + "max_output_tokens": 327680, + "input_cost_per_token": 8e-08, + "output_cost_per_token": 3e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/meta-llama/Llama-Guard-3-8B": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 5.5e-08, + "output_cost_per_token": 5.5e-08, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": false + }, + "deepinfra/meta-llama/Llama-Guard-4-12B": { + "max_tokens": 163840, + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "input_cost_per_token": 1.8e-07, + "output_cost_per_token": 1.8e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": false + }, + "deepinfra/meta-llama/Meta-Llama-3-8B-Instruct": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 3e-08, + "output_cost_per_token": 6e-08, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/meta-llama/Meta-Llama-3.1-70B-Instruct": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 4e-07, + "output_cost_per_token": 4e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 2.8e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/meta-llama/Meta-Llama-3.1-8B-Instruct": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 3e-08, + "output_cost_per_token": 5e-08, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 2e-08, + "output_cost_per_token": 3e-08, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/microsoft/WizardLM-2-8x22B": { + "max_tokens": 65536, + "max_input_tokens": 65536, + "max_output_tokens": 65536, + "input_cost_per_token": 4.8e-07, + "output_cost_per_token": 4.8e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": false + }, + "deepinfra/microsoft/phi-4": { + "max_tokens": 16384, + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "input_cost_per_token": 7e-08, + "output_cost_per_token": 1.4e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/mistralai/Mistral-Nemo-Instruct-2407": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 2e-08, + "output_cost_per_token": 4e-08, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/mistralai/Mistral-Small-24B-Instruct-2501": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 5e-08, + "output_cost_per_token": 8e-08, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/mistralai/Mistral-Small-3.2-24B-Instruct-2506": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "input_cost_per_token": 7.5e-08, + "output_cost_per_token": 2e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/mistralai/Mixtral-8x7B-Instruct-v0.1": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 4e-07, + "output_cost_per_token": 4e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/moonshotai/Kimi-K2-Instruct": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 5e-07, + "output_cost_per_token": 2e-06, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/moonshotai/Kimi-K2-Instruct-0905": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 5e-07, + "output_cost_per_token": 2e-06, + "cache_read_input_token_cost": 4e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/nvidia/Llama-3.1-Nemotron-70B-Instruct": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 6e-07, + "output_cost_per_token": 6e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/nvidia/Llama-3.3-Nemotron-Super-49B-v1.5": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 4e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/nvidia/NVIDIA-Nemotron-Nano-9B-v2": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 4e-08, + "output_cost_per_token": 1.6e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/openai/gpt-oss-120b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 5e-08, + "output_cost_per_token": 4.5e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/openai/gpt-oss-20b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 4e-08, + "output_cost_per_token": 1.5e-07, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepinfra/zai-org/GLM-4.5": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 4e-07, + "output_cost_per_token": 1.6e-06, + "litellm_provider": "deepinfra", + "mode": "chat", + "supports_tool_choice": true + }, + "deepseek/deepseek-chat": { + "cache_creation_input_token_cost": 0.0, + "cache_read_input_token_cost": 7e-08, + "input_cost_per_token": 2.7e-07, + "input_cost_per_token_cache_hit": 7e-08, + "litellm_provider": "deepseek", + "max_input_tokens": 65536, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.1e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_tool_choice": true + }, + "deepseek/deepseek-coder": { + "input_cost_per_token": 1.4e-07, + "input_cost_per_token_cache_hit": 1.4e-08, + "litellm_provider": "deepseek", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 2.8e-07, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_tool_choice": true + }, + "deepseek/deepseek-r1": { + "input_cost_per_token": 5.5e-07, + "input_cost_per_token_cache_hit": 1.4e-07, + "litellm_provider": "deepseek", + "max_input_tokens": 65536, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2.19e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "deepseek/deepseek-reasoner": { + "input_cost_per_token": 5.5e-07, + "input_cost_per_token_cache_hit": 1.4e-07, + "litellm_provider": "deepseek", + "max_input_tokens": 65536, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2.19e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "deepseek/deepseek-v3": { + "cache_creation_input_token_cost": 0.0, + "cache_read_input_token_cost": 7e-08, + "input_cost_per_token": 2.7e-07, + "input_cost_per_token_cache_hit": 7e-08, + "litellm_provider": "deepseek", + "max_input_tokens": 65536, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.1e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_tool_choice": true + }, + "deepseek/deepseek-v3.2": { + "input_cost_per_token": 2.8e-07, + "input_cost_per_token_cache_hit": 2.8e-08, + "litellm_provider": "deepseek", + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "deepseek.v3-v1:0": { + "input_cost_per_token": 5.8e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 163840, + "max_output_tokens": 81920, + "max_tokens": 163840, + "mode": "chat", + "output_cost_per_token": 1.68e-06, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "dolphin": { + "input_cost_per_token": 5e-07, + "litellm_provider": "nlp_cloud", + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "completion", + "output_cost_per_token": 5e-07 + }, + "doubao-embedding": { + "input_cost_per_token": 0.0, + "litellm_provider": "volcengine", + "max_input_tokens": 4096, + "max_tokens": 4096, + "metadata": { + "notes": "Volcengine Doubao embedding model - standard version with 2560 dimensions" + }, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 2560 + }, + "doubao-embedding-large": { + "input_cost_per_token": 0.0, + "litellm_provider": "volcengine", + "max_input_tokens": 4096, + "max_tokens": 4096, + "metadata": { + "notes": "Volcengine Doubao embedding model - large version with 2048 dimensions" + }, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 2048 + }, + "doubao-embedding-large-text-240915": { + "input_cost_per_token": 0.0, + "litellm_provider": "volcengine", + "max_input_tokens": 4096, + "max_tokens": 4096, + "metadata": { + "notes": "Volcengine Doubao embedding model - text-240915 version with 4096 dimensions" + }, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 4096 + }, + "doubao-embedding-large-text-250515": { + "input_cost_per_token": 0.0, + "litellm_provider": "volcengine", + "max_input_tokens": 4096, + "max_tokens": 4096, + "metadata": { + "notes": "Volcengine Doubao embedding model - text-250515 version with 2048 dimensions" + }, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 2048 + }, + "doubao-embedding-text-240715": { + "input_cost_per_token": 0.0, + "litellm_provider": "volcengine", + "max_input_tokens": 4096, + "max_tokens": 4096, + "metadata": { + "notes": "Volcengine Doubao embedding model - text-240715 version with 2560 dimensions" + }, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 2560 + }, + "exa_ai/search": { + "litellm_provider": "exa_ai", + "mode": "search", + "tiered_pricing": [ + { + "input_cost_per_query": 5e-03, + "max_results_range": [ + 0, + 25 + ] + }, + { + "input_cost_per_query": 25e-03, + "max_results_range": [ + 26, + 100 + ] + } + ] + }, + "firecrawl/search": { + "litellm_provider": "firecrawl", + "mode": "search", + "tiered_pricing": [ + { + "input_cost_per_query": 1.66e-03, + "max_results_range": [ + 1, + 10 + ] + }, + { + "input_cost_per_query": 3.32e-03, + "max_results_range": [ + 11, + 20 + ] + }, + { + "input_cost_per_query": 4.98e-03, + "max_results_range": [ + 21, + 30 + ] + }, + { + "input_cost_per_query": 6.64e-03, + "max_results_range": [ + 31, + 40 + ] + }, + { + "input_cost_per_query": 8.3e-03, + "max_results_range": [ + 41, + 50 + ] + }, + { + "input_cost_per_query": 9.96e-03, + "max_results_range": [ + 51, + 60 + ] + }, + { + "input_cost_per_query": 11.62e-03, + "max_results_range": [ + 61, + 70 + ] + }, + { + "input_cost_per_query": 13.28e-03, + "max_results_range": [ + 71, + 80 + ] + }, + { + "input_cost_per_query": 14.94e-03, + "max_results_range": [ + 81, + 90 + ] + }, + { + "input_cost_per_query": 16.6e-03, + "max_results_range": [ + 91, + 100 + ] + } + ], + "metadata": { + "notes": "Firecrawl search pricing: $83 for 100,000 credits, 2 credits per 10 results. Cost = ceiling(limit/10) * 2 * $0.00083" + } + }, + "perplexity/search": { + "input_cost_per_query": 5e-03, + "litellm_provider": "perplexity", + "mode": "search" + }, + "searxng/search": { + "litellm_provider": "searxng", + "mode": "search", + "input_cost_per_query": 0.0, + "metadata": { + "notes": "SearXNG is an open-source metasearch engine. Free to use when self-hosted or using public instances." + } + }, + "elevenlabs/scribe_v1": { + "input_cost_per_second": 6.11e-05, + "litellm_provider": "elevenlabs", + "metadata": { + "calculation": "$0.22/hour = $0.00366/minute = $0.0000611 per second (enterprise pricing)", + "notes": "ElevenLabs Scribe v1 - state-of-the-art speech recognition model with 99 language support", + "original_pricing_per_hour": 0.22 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://elevenlabs.io/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "elevenlabs/scribe_v1_experimental": { + "input_cost_per_second": 6.11e-05, + "litellm_provider": "elevenlabs", + "metadata": { + "calculation": "$0.22/hour = $0.00366/minute = $0.0000611 per second (enterprise pricing)", + "notes": "ElevenLabs Scribe v1 experimental - enhanced version of the main Scribe model", + "original_pricing_per_hour": 0.22 + }, + "mode": "audio_transcription", + "output_cost_per_second": 0.0, + "source": "https://elevenlabs.io/pricing", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "embed-english-light-v2.0": { + "input_cost_per_token": 1e-07, + "litellm_provider": "cohere", + "max_input_tokens": 1024, + "max_tokens": 1024, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "embed-english-light-v3.0": { + "input_cost_per_token": 1e-07, + "litellm_provider": "cohere", + "max_input_tokens": 1024, + "max_tokens": 1024, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "embed-english-v2.0": { + "input_cost_per_token": 1e-07, + "litellm_provider": "cohere", + "max_input_tokens": 4096, + "max_tokens": 4096, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "embed-english-v3.0": { + "input_cost_per_image": 0.0001, + "input_cost_per_token": 1e-07, + "litellm_provider": "cohere", + "max_input_tokens": 1024, + "max_tokens": 1024, + "metadata": { + "notes": "'supports_image_input' is a deprecated field. Use 'supports_embedding_image_input' instead." + }, + "mode": "embedding", + "output_cost_per_token": 0.0, + "supports_embedding_image_input": true, + "supports_image_input": true + }, + "embed-multilingual-v2.0": { + "input_cost_per_token": 1e-07, + "litellm_provider": "cohere", + "max_input_tokens": 768, + "max_tokens": 768, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "embed-multilingual-v3.0": { + "input_cost_per_token": 1e-07, + "litellm_provider": "cohere", + "max_input_tokens": 1024, + "max_tokens": 1024, + "mode": "embedding", + "output_cost_per_token": 0.0, + "supports_embedding_image_input": true + }, + "embed-multilingual-light-v3.0": { + "input_cost_per_token": 1e-04, + "litellm_provider": "cohere", + "max_input_tokens": 1024, + "max_tokens": 1024, + "mode": "embedding", + "output_cost_per_token": 0.0, + "supports_embedding_image_input": true + }, + "eu.amazon.nova-lite-v1:0": { + "input_cost_per_token": 7.8e-08, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 300000, + "max_output_tokens": 10000, + "max_tokens": 10000, + "mode": "chat", + "output_cost_per_token": 3.12e-07, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_vision": true + }, + "eu.amazon.nova-micro-v1:0": { + "input_cost_per_token": 4.6e-08, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 10000, + "max_tokens": 10000, + "mode": "chat", + "output_cost_per_token": 1.84e-07, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true + }, + "eu.amazon.nova-pro-v1:0": { + "input_cost_per_token": 1.05e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 300000, + "max_output_tokens": 10000, + "max_tokens": 10000, + "mode": "chat", + "output_cost_per_token": 4.2e-06, + "source": "https://aws.amazon.com/bedrock/pricing/", + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_vision": true + }, + "eu.anthropic.claude-3-5-haiku-20241022-v1:0": { + "input_cost_per_token": 2.5e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.25e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "eu.anthropic.claude-haiku-4-5-20251001-v1:0": { + "cache_creation_input_token_cost": 1.375e-06, + "cache_read_input_token_cost": 1.1e-07, + "input_cost_per_token": 1.1e-06, + "deprecation_date": "2026-10-15", + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 5.5e-06, + "source": "https://aws.amazon.com/about-aws/whats-new/2025/10/claude-4-5-haiku-anthropic-amazon-bedrock", + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 346 + }, + "eu.anthropic.claude-3-5-sonnet-20240620-v1:0": { + "input_cost_per_token": 3e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "eu.anthropic.claude-3-5-sonnet-20241022-v2:0": { + "input_cost_per_token": 3e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "eu.anthropic.claude-3-7-sonnet-20250219-v1:0": { + "input_cost_per_token": 3e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "eu.anthropic.claude-3-haiku-20240307-v1:0": { + "input_cost_per_token": 2.5e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.25e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "eu.anthropic.claude-3-opus-20240229-v1:0": { + "input_cost_per_token": 1.5e-05, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "eu.anthropic.claude-3-sonnet-20240229-v1:0": { + "input_cost_per_token": 3e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "eu.anthropic.claude-opus-4-1-20250805-v1:0": { + "cache_creation_input_token_cost": 1.875e-05, + "cache_read_input_token_cost": 1.5e-06, + "input_cost_per_token": 1.5e-05, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "eu.anthropic.claude-opus-4-20250514-v1:0": { + "cache_creation_input_token_cost": 1.875e-05, + "cache_read_input_token_cost": 1.5e-06, + "input_cost_per_token": 1.5e-05, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "eu.anthropic.claude-sonnet-4-20250514-v1:0": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "eu.anthropic.claude-sonnet-4-5-20250929-v1:0": { + "cache_creation_input_token_cost": 4.125e-06, + "cache_read_input_token_cost": 3.3e-07, + "input_cost_per_token": 3.3e-06, + "input_cost_per_token_above_200k_tokens": 6.6e-06, + "output_cost_per_token_above_200k_tokens": 2.475e-05, + "cache_creation_input_token_cost_above_200k_tokens": 8.25e-06, + "cache_read_input_token_cost_above_200k_tokens": 6.6e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.65e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 346 + }, + "eu.meta.llama3-2-1b-instruct-v1:0": { + "input_cost_per_token": 1.3e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.3e-07, + "supports_function_calling": true, + "supports_tool_choice": false + }, + "eu.meta.llama3-2-3b-instruct-v1:0": { + "input_cost_per_token": 1.9e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.9e-07, + "supports_function_calling": true, + "supports_tool_choice": false + }, + "eu.mistral.pixtral-large-2502-v1:0": { + "input_cost_per_token": 2e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 6e-06, + "supports_function_calling": true, + "supports_tool_choice": false + }, + "fal_ai/bria/text-to-image/3.2": { + "litellm_provider": "fal_ai", + "mode": "image_generation", + "output_cost_per_image": 0.0398, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "fal_ai/fal-ai/flux-pro/v1.1": { + "litellm_provider": "fal_ai", + "mode": "image_generation", + "output_cost_per_image": 0.04, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "fal_ai/fal-ai/flux-pro/v1.1-ultra": { + "litellm_provider": "fal_ai", + "mode": "image_generation", + "output_cost_per_image": 0.06, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "fal_ai/fal-ai/flux/schnell": { + "litellm_provider": "fal_ai", + "mode": "image_generation", + "output_cost_per_image": 0.003, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "fal_ai/fal-ai/bytedance/seedream/v3/text-to-image": { + "litellm_provider": "fal_ai", + "mode": "image_generation", + "output_cost_per_image": 0.03, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "fal_ai/fal-ai/bytedance/dreamina/v3.1/text-to-image": { + "litellm_provider": "fal_ai", + "mode": "image_generation", + "output_cost_per_image": 0.03, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "fal_ai/fal-ai/ideogram/v3": { + "litellm_provider": "fal_ai", + "mode": "image_generation", + "output_cost_per_image": 0.06, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "fal_ai/fal-ai/imagen4/preview": { + "litellm_provider": "fal_ai", + "mode": "image_generation", + "output_cost_per_image": 0.0398, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "fal_ai/fal-ai/imagen4/preview/fast": { + "litellm_provider": "fal_ai", + "mode": "image_generation", + "output_cost_per_image": 0.02, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "fal_ai/fal-ai/imagen4/preview/ultra": { + "litellm_provider": "fal_ai", + "mode": "image_generation", + "output_cost_per_image": 0.06, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "fal_ai/fal-ai/recraft/v3/text-to-image": { + "litellm_provider": "fal_ai", + "mode": "image_generation", + "output_cost_per_image": 0.0398, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "fal_ai/fal-ai/stable-diffusion-v35-medium": { + "litellm_provider": "fal_ai", + "mode": "image_generation", + "output_cost_per_image": 0.0398, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "featherless_ai/featherless-ai/Qwerky-72B": { + "litellm_provider": "featherless_ai", + "max_input_tokens": 32768, + "max_output_tokens": 4096, + "max_tokens": 32768, + "mode": "chat" + }, + "featherless_ai/featherless-ai/Qwerky-QwQ-32B": { + "litellm_provider": "featherless_ai", + "max_input_tokens": 32768, + "max_output_tokens": 4096, + "max_tokens": 32768, + "mode": "chat" + }, + "fireworks-ai-4.1b-to-16b": { + "input_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "output_cost_per_token": 2e-07 + }, + "fireworks-ai-56b-to-176b": { + "input_cost_per_token": 1.2e-06, + "litellm_provider": "fireworks_ai", + "output_cost_per_token": 1.2e-06 + }, + "fireworks-ai-above-16b": { + "input_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "output_cost_per_token": 9e-07 + }, + "fireworks-ai-default": { + "input_cost_per_token": 0.0, + "litellm_provider": "fireworks_ai", + "output_cost_per_token": 0.0 + }, + "fireworks-ai-embedding-150m-to-350m": { + "input_cost_per_token": 1.6e-08, + "litellm_provider": "fireworks_ai-embedding-models", + "output_cost_per_token": 0.0 + }, + "fireworks-ai-embedding-up-to-150m": { + "input_cost_per_token": 8e-09, + "litellm_provider": "fireworks_ai-embedding-models", + "output_cost_per_token": 0.0 + }, + "fireworks-ai-moe-up-to-56b": { + "input_cost_per_token": 5e-07, + "litellm_provider": "fireworks_ai", + "output_cost_per_token": 5e-07 + }, + "fireworks-ai-up-to-4b": { + "input_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "output_cost_per_token": 2e-07 + }, + "fireworks_ai/WhereIsAI/UAE-Large-V1": { + "input_cost_per_token": 1.6e-08, + "litellm_provider": "fireworks_ai-embedding-models", + "max_input_tokens": 512, + "max_tokens": 512, + "mode": "embedding", + "output_cost_per_token": 0.0, + "source": "https://fireworks.ai/pricing" + }, + "fireworks_ai/accounts/fireworks/models/deepseek-coder-v2-instruct": { + "input_cost_per_token": 1.2e-06, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 65536, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "source": "https://fireworks.ai/pricing", + "supports_function_calling": false, + "supports_response_schema": true, + "supports_tool_choice": false + }, + "fireworks_ai/accounts/fireworks/models/deepseek-r1": { + "input_cost_per_token": 3e-06, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 128000, + "max_output_tokens": 20480, + "max_tokens": 20480, + "mode": "chat", + "output_cost_per_token": 8e-06, + "source": "https://fireworks.ai/pricing", + "supports_response_schema": true, + "supports_tool_choice": false + }, + "fireworks_ai/accounts/fireworks/models/deepseek-r1-0528": { + "input_cost_per_token": 3e-06, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 160000, + "max_output_tokens": 160000, + "max_tokens": 160000, + "mode": "chat", + "output_cost_per_token": 8e-06, + "source": "https://fireworks.ai/pricing", + "supports_response_schema": true, + "supports_tool_choice": false + }, + "fireworks_ai/accounts/fireworks/models/deepseek-r1-basic": { + "input_cost_per_token": 5.5e-07, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 128000, + "max_output_tokens": 20480, + "max_tokens": 20480, + "mode": "chat", + "output_cost_per_token": 2.19e-06, + "source": "https://fireworks.ai/pricing", + "supports_response_schema": true, + "supports_tool_choice": false + }, + "fireworks_ai/accounts/fireworks/models/deepseek-v3": { + "input_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 9e-07, + "source": "https://fireworks.ai/pricing", + "supports_response_schema": true, + "supports_tool_choice": false + }, + "fireworks_ai/accounts/fireworks/models/deepseek-v3-0324": { + "input_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "max_tokens": 163840, + "mode": "chat", + "output_cost_per_token": 9e-07, + "source": "https://fireworks.ai/models/fireworks/deepseek-v3-0324", + "supports_response_schema": true, + "supports_tool_choice": false + }, + "fireworks_ai/accounts/fireworks/models/deepseek-v3p1": { + "input_cost_per_token": 5.6e-07, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.68e-06, + "source": "https://fireworks.ai/pricing", + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "fireworks_ai/accounts/fireworks/models/deepseek-v3p1-terminus": { + "input_cost_per_token": 5.6e-07, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.68e-06, + "source": "https://fireworks.ai/pricing", + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "fireworks_ai/accounts/fireworks/models/deepseek-v3p2": { + "input_cost_per_token": 1.2e-06, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "max_tokens": 163840, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "source": "https://fireworks.ai/models/fireworks/deepseek-v3p2", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "fireworks_ai/accounts/fireworks/models/firefunction-v2": { + "input_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 9e-07, + "source": "https://fireworks.ai/pricing", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "fireworks_ai/accounts/fireworks/models/glm-4p5": { + "input_cost_per_token": 5.5e-07, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 128000, + "max_output_tokens": 96000, + "max_tokens": 96000, + "mode": "chat", + "output_cost_per_token": 2.19e-06, + "source": "https://fireworks.ai/models/fireworks/glm-4p5", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "fireworks_ai/accounts/fireworks/models/glm-4p5-air": { + "input_cost_per_token": 2.2e-07, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 128000, + "max_output_tokens": 96000, + "max_tokens": 96000, + "mode": "chat", + "output_cost_per_token": 8.8e-07, + "source": "https://artificialanalysis.ai/models/glm-4-5-air", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "fireworks_ai/accounts/fireworks/models/glm-4p6": { + "input_cost_per_token": 0.55e-06, + "output_cost_per_token": 2.19e-06, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 202800, + "max_output_tokens": 202800, + "max_tokens": 202800, + "mode": "chat", + "source": "https://fireworks.ai/pricing", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "fireworks_ai/accounts/fireworks/models/gpt-oss-120b": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 6e-07, + "source": "https://fireworks.ai/pricing", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "fireworks_ai/accounts/fireworks/models/gpt-oss-20b": { + "input_cost_per_token": 5e-08, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2e-07, + "source": "https://fireworks.ai/pricing", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "fireworks_ai/accounts/fireworks/models/kimi-k2-instruct": { + "input_cost_per_token": 6e-07, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 131072, + "max_output_tokens": 16384, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2.5e-06, + "source": "https://fireworks.ai/models/fireworks/kimi-k2-instruct", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "fireworks_ai/accounts/fireworks/models/kimi-k2-instruct-0905": { + "input_cost_per_token": 6e-07, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 262144, + "max_output_tokens": 32768, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 2.5e-06, + "source": "https://app.fireworks.ai/models/fireworks/kimi-k2-instruct-0905", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "fireworks_ai/accounts/fireworks/models/kimi-k2-thinking": { + "input_cost_per_token": 6e-07, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 2.5e-06, + "source": "https://fireworks.ai/pricing", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "fireworks_ai/accounts/fireworks/models/llama-v3p1-405b-instruct": { + "input_cost_per_token": 3e-06, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 3e-06, + "source": "https://fireworks.ai/pricing", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "fireworks_ai/accounts/fireworks/models/llama-v3p1-8b-instruct": { + "input_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1e-07, + "source": "https://fireworks.ai/pricing", + "supports_function_calling": false, + "supports_response_schema": true, + "supports_tool_choice": false + }, + "fireworks_ai/accounts/fireworks/models/llama-v3p2-11b-vision-instruct": { + "input_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 2e-07, + "source": "https://fireworks.ai/pricing", + "supports_function_calling": false, + "supports_response_schema": true, + "supports_tool_choice": false, + "supports_vision": true + }, + "fireworks_ai/accounts/fireworks/models/llama-v3p2-1b-instruct": { + "input_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1e-07, + "source": "https://fireworks.ai/pricing", + "supports_function_calling": false, + "supports_response_schema": true, + "supports_tool_choice": false + }, + "fireworks_ai/accounts/fireworks/models/llama-v3p2-3b-instruct": { + "input_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1e-07, + "source": "https://fireworks.ai/pricing", + "supports_function_calling": false, + "supports_response_schema": true, + "supports_tool_choice": false + }, + "fireworks_ai/accounts/fireworks/models/llama-v3p2-90b-vision-instruct": { + "input_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 9e-07, + "source": "https://fireworks.ai/pricing", + "supports_response_schema": true, + "supports_tool_choice": false, + "supports_vision": true + }, + "fireworks_ai/accounts/fireworks/models/llama4-maverick-instruct-basic": { + "input_cost_per_token": 2.2e-07, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 8.8e-07, + "source": "https://fireworks.ai/pricing", + "supports_response_schema": true, + "supports_tool_choice": false + }, + "fireworks_ai/accounts/fireworks/models/llama4-scout-instruct-basic": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 6e-07, + "source": "https://fireworks.ai/pricing", + "supports_response_schema": true, + "supports_tool_choice": false + }, + "fireworks_ai/accounts/fireworks/models/mixtral-8x22b-instruct-hf": { + "input_cost_per_token": 1.2e-06, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 65536, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "source": "https://fireworks.ai/pricing", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "fireworks_ai/accounts/fireworks/models/qwen2-72b-instruct": { + "input_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 9e-07, + "source": "https://fireworks.ai/pricing", + "supports_function_calling": false, + "supports_response_schema": true, + "supports_tool_choice": false + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-coder-32b-instruct": { + "input_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 9e-07, + "source": "https://fireworks.ai/pricing", + "supports_function_calling": false, + "supports_response_schema": true, + "supports_tool_choice": false + }, + "fireworks_ai/accounts/fireworks/models/yi-large": { + "input_cost_per_token": 3e-06, + "litellm_provider": "fireworks_ai", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 3e-06, + "source": "https://fireworks.ai/pricing", + "supports_function_calling": false, + "supports_response_schema": true, + "supports_tool_choice": false + }, + "fireworks_ai/nomic-ai/nomic-embed-text-v1": { + "input_cost_per_token": 8e-09, + "litellm_provider": "fireworks_ai-embedding-models", + "max_input_tokens": 8192, + "max_tokens": 8192, + "mode": "embedding", + "output_cost_per_token": 0.0, + "source": "https://fireworks.ai/pricing" + }, + "fireworks_ai/nomic-ai/nomic-embed-text-v1.5": { + "input_cost_per_token": 8e-09, + "litellm_provider": "fireworks_ai-embedding-models", + "max_input_tokens": 8192, + "max_tokens": 8192, + "mode": "embedding", + "output_cost_per_token": 0.0, + "source": "https://fireworks.ai/pricing" + }, + "fireworks_ai/thenlper/gte-base": { + "input_cost_per_token": 8e-09, + "litellm_provider": "fireworks_ai-embedding-models", + "max_input_tokens": 512, + "max_tokens": 512, + "mode": "embedding", + "output_cost_per_token": 0.0, + "source": "https://fireworks.ai/pricing" + }, + "fireworks_ai/thenlper/gte-large": { + "input_cost_per_token": 1.6e-08, + "litellm_provider": "fireworks_ai-embedding-models", + "max_input_tokens": 512, + "max_tokens": 512, + "mode": "embedding", + "output_cost_per_token": 0.0, + "source": "https://fireworks.ai/pricing" + }, + "friendliai/meta-llama-3.1-70b-instruct": { + "input_cost_per_token": 6e-07, + "litellm_provider": "friendliai", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 6e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "friendliai/meta-llama-3.1-8b-instruct": { + "input_cost_per_token": 1e-07, + "litellm_provider": "friendliai", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "ft:babbage-002": { + "input_cost_per_token": 1.6e-06, + "input_cost_per_token_batches": 2e-07, + "litellm_provider": "text-completion-openai", + "max_input_tokens": 16384, + "max_output_tokens": 4096, + "max_tokens": 16384, + "mode": "completion", + "output_cost_per_token": 1.6e-06, + "output_cost_per_token_batches": 2e-07 + }, + "ft:davinci-002": { + "input_cost_per_token": 1.2e-05, + "input_cost_per_token_batches": 1e-06, + "litellm_provider": "text-completion-openai", + "max_input_tokens": 16384, + "max_output_tokens": 4096, + "max_tokens": 16384, + "mode": "completion", + "output_cost_per_token": 1.2e-05, + "output_cost_per_token_batches": 1e-06 + }, + "ft:gpt-3.5-turbo": { + "input_cost_per_token": 3e-06, + "input_cost_per_token_batches": 1.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 16385, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 6e-06, + "output_cost_per_token_batches": 3e-06, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "ft:gpt-3.5-turbo-0125": { + "input_cost_per_token": 3e-06, + "litellm_provider": "openai", + "max_input_tokens": 16385, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 6e-06, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "ft:gpt-3.5-turbo-0613": { + "input_cost_per_token": 3e-06, + "litellm_provider": "openai", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 6e-06, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "ft:gpt-3.5-turbo-1106": { + "input_cost_per_token": 3e-06, + "litellm_provider": "openai", + "max_input_tokens": 16385, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 6e-06, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "ft:gpt-4-0613": { + "input_cost_per_token": 3e-05, + "litellm_provider": "openai", + "max_input_tokens": 8192, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 6e-05, + "source": "OpenAI needs to add pricing for this ft model, will be updated when added by OpenAI. Defaulting to base model pricing", + "supports_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "ft:gpt-4o-2024-08-06": { + "cache_read_input_token_cost": 1.875e-06, + "input_cost_per_token": 3.75e-06, + "input_cost_per_token_batches": 1.875e-06, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "output_cost_per_token_batches": 7.5e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "ft:gpt-4o-2024-11-20": { + "cache_creation_input_token_cost": 1.875e-06, + "input_cost_per_token": 3.75e-06, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "ft:gpt-4o-mini-2024-07-18": { + "cache_read_input_token_cost": 1.5e-07, + "input_cost_per_token": 3e-07, + "input_cost_per_token_batches": 1.5e-07, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "output_cost_per_token_batches": 6e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "ft:gpt-4.1-2025-04-14": { + "cache_read_input_token_cost": 7.5e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_batches": 1.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 1.2e-05, + "output_cost_per_token_batches": 6e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "ft:gpt-4.1-mini-2025-04-14": { + "cache_read_input_token_cost": 2e-07, + "input_cost_per_token": 8e-07, + "input_cost_per_token_batches": 4e-07, + "litellm_provider": "openai", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 3.2e-06, + "output_cost_per_token_batches": 1.6e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "ft:gpt-4.1-nano-2025-04-14": { + "cache_read_input_token_cost": 5e-08, + "input_cost_per_token": 2e-07, + "input_cost_per_token_batches": 1e-07, + "litellm_provider": "openai", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 8e-07, + "output_cost_per_token_batches": 4e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "ft:o4-mini-2025-04-16": { + "cache_read_input_token_cost": 1e-06, + "input_cost_per_token": 4e-06, + "input_cost_per_token_batches": 2e-06, + "litellm_provider": "openai", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 1.6e-05, + "output_cost_per_token_batches": 8e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": false, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "gemini-1.0-pro": { + "input_cost_per_character": 1.25e-07, + "input_cost_per_image": 0.0025, + "input_cost_per_token": 5e-07, + "input_cost_per_video_per_second": 0.002, + "litellm_provider": "vertex_ai-language-models", + "max_input_tokens": 32760, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_character": 3.75e-07, + "output_cost_per_token": 1.5e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#google_models", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "gemini-1.0-pro-001": { + "deprecation_date": "2025-04-09", + "input_cost_per_character": 1.25e-07, + "input_cost_per_image": 0.0025, + "input_cost_per_token": 5e-07, + "input_cost_per_video_per_second": 0.002, + "litellm_provider": "vertex_ai-language-models", + "max_input_tokens": 32760, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_character": 3.75e-07, + "output_cost_per_token": 1.5e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "gemini-1.0-pro-002": { + "deprecation_date": "2025-04-09", + "input_cost_per_character": 1.25e-07, + "input_cost_per_image": 0.0025, + "input_cost_per_token": 5e-07, + "input_cost_per_video_per_second": 0.002, + "litellm_provider": "vertex_ai-language-models", + "max_input_tokens": 32760, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_character": 3.75e-07, + "output_cost_per_token": 1.5e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "gemini-1.0-pro-vision": { + "input_cost_per_image": 0.0025, + "input_cost_per_token": 5e-07, + "litellm_provider": "vertex_ai-vision-models", + "max_images_per_prompt": 16, + "max_input_tokens": 16384, + "max_output_tokens": 2048, + "max_tokens": 2048, + "max_video_length": 2, + "max_videos_per_prompt": 1, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gemini-1.0-pro-vision-001": { + "deprecation_date": "2025-04-09", + "input_cost_per_image": 0.0025, + "input_cost_per_token": 5e-07, + "litellm_provider": "vertex_ai-vision-models", + "max_images_per_prompt": 16, + "max_input_tokens": 16384, + "max_output_tokens": 2048, + "max_tokens": 2048, + "max_video_length": 2, + "max_videos_per_prompt": 1, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gemini-1.0-ultra": { + "input_cost_per_character": 1.25e-07, + "input_cost_per_image": 0.0025, + "input_cost_per_token": 5e-07, + "input_cost_per_video_per_second": 0.002, + "litellm_provider": "vertex_ai-language-models", + "max_input_tokens": 8192, + "max_output_tokens": 2048, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_character": 3.75e-07, + "output_cost_per_token": 1.5e-06, + "source": "As of Jun, 2024. There is no available doc on vertex ai pricing gemini-1.0-ultra-001. Using gemini-1.0-pro pricing. Got max_tokens info here: https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "gemini-1.0-ultra-001": { + "input_cost_per_character": 1.25e-07, + "input_cost_per_image": 0.0025, + "input_cost_per_token": 5e-07, + "input_cost_per_video_per_second": 0.002, + "litellm_provider": "vertex_ai-language-models", + "max_input_tokens": 8192, + "max_output_tokens": 2048, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_character": 3.75e-07, + "output_cost_per_token": 1.5e-06, + "source": "As of Jun, 2024. There is no available doc on vertex ai pricing gemini-1.0-ultra-001. Using gemini-1.0-pro pricing. Got max_tokens info here: https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "gemini-1.5-flash": { + "input_cost_per_audio_per_second": 2e-06, + "input_cost_per_audio_per_second_above_128k_tokens": 4e-06, + "input_cost_per_character": 1.875e-08, + "input_cost_per_character_above_128k_tokens": 2.5e-07, + "input_cost_per_image": 2e-05, + "input_cost_per_image_above_128k_tokens": 4e-05, + "input_cost_per_token": 7.5e-08, + "input_cost_per_token_above_128k_tokens": 1e-06, + "input_cost_per_video_per_second": 2e-05, + "input_cost_per_video_per_second_above_128k_tokens": 4e-05, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1000000, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_character": 7.5e-08, + "output_cost_per_character_above_128k_tokens": 1.5e-07, + "output_cost_per_token": 3e-07, + "output_cost_per_token_above_128k_tokens": 6e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gemini-1.5-flash-001": { + "deprecation_date": "2025-05-24", + "input_cost_per_audio_per_second": 2e-06, + "input_cost_per_audio_per_second_above_128k_tokens": 4e-06, + "input_cost_per_character": 1.875e-08, + "input_cost_per_character_above_128k_tokens": 2.5e-07, + "input_cost_per_image": 2e-05, + "input_cost_per_image_above_128k_tokens": 4e-05, + "input_cost_per_token": 7.5e-08, + "input_cost_per_token_above_128k_tokens": 1e-06, + "input_cost_per_video_per_second": 2e-05, + "input_cost_per_video_per_second_above_128k_tokens": 4e-05, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1000000, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_character": 7.5e-08, + "output_cost_per_character_above_128k_tokens": 1.5e-07, + "output_cost_per_token": 3e-07, + "output_cost_per_token_above_128k_tokens": 6e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gemini-1.5-flash-002": { + "deprecation_date": "2025-09-24", + "input_cost_per_audio_per_second": 2e-06, + "input_cost_per_audio_per_second_above_128k_tokens": 4e-06, + "input_cost_per_character": 1.875e-08, + "input_cost_per_character_above_128k_tokens": 2.5e-07, + "input_cost_per_image": 2e-05, + "input_cost_per_image_above_128k_tokens": 4e-05, + "input_cost_per_token": 7.5e-08, + "input_cost_per_token_above_128k_tokens": 1e-06, + "input_cost_per_video_per_second": 2e-05, + "input_cost_per_video_per_second_above_128k_tokens": 4e-05, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_character": 7.5e-08, + "output_cost_per_character_above_128k_tokens": 1.5e-07, + "output_cost_per_token": 3e-07, + "output_cost_per_token_above_128k_tokens": 6e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-1.5-flash", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gemini-1.5-flash-exp-0827": { + "input_cost_per_audio_per_second": 2e-06, + "input_cost_per_audio_per_second_above_128k_tokens": 4e-06, + "input_cost_per_character": 1.875e-08, + "input_cost_per_character_above_128k_tokens": 2.5e-07, + "input_cost_per_image": 2e-05, + "input_cost_per_image_above_128k_tokens": 4e-05, + "input_cost_per_token": 4.688e-09, + "input_cost_per_token_above_128k_tokens": 1e-06, + "input_cost_per_video_per_second": 2e-05, + "input_cost_per_video_per_second_above_128k_tokens": 4e-05, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1000000, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_character": 1.875e-08, + "output_cost_per_character_above_128k_tokens": 3.75e-08, + "output_cost_per_token": 4.6875e-09, + "output_cost_per_token_above_128k_tokens": 9.375e-09, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gemini-1.5-flash-preview-0514": { + "input_cost_per_audio_per_second": 2e-06, + "input_cost_per_audio_per_second_above_128k_tokens": 4e-06, + "input_cost_per_character": 1.875e-08, + "input_cost_per_character_above_128k_tokens": 2.5e-07, + "input_cost_per_image": 2e-05, + "input_cost_per_image_above_128k_tokens": 4e-05, + "input_cost_per_token": 7.5e-08, + "input_cost_per_token_above_128k_tokens": 1e-06, + "input_cost_per_video_per_second": 2e-05, + "input_cost_per_video_per_second_above_128k_tokens": 4e-05, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1000000, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_character": 1.875e-08, + "output_cost_per_character_above_128k_tokens": 3.75e-08, + "output_cost_per_token": 4.6875e-09, + "output_cost_per_token_above_128k_tokens": 9.375e-09, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gemini-1.5-pro": { + "input_cost_per_audio_per_second": 3.125e-05, + "input_cost_per_audio_per_second_above_128k_tokens": 6.25e-05, + "input_cost_per_character": 3.125e-07, + "input_cost_per_character_above_128k_tokens": 6.25e-07, + "input_cost_per_image": 0.00032875, + "input_cost_per_image_above_128k_tokens": 0.0006575, + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_above_128k_tokens": 2.5e-06, + "input_cost_per_video_per_second": 0.00032875, + "input_cost_per_video_per_second_above_128k_tokens": 0.0006575, + "litellm_provider": "vertex_ai-language-models", + "max_input_tokens": 2097152, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_character": 1.25e-06, + "output_cost_per_character_above_128k_tokens": 2.5e-06, + "output_cost_per_token": 5e-06, + "output_cost_per_token_above_128k_tokens": 1e-05, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gemini-1.5-pro-001": { + "deprecation_date": "2025-05-24", + "input_cost_per_audio_per_second": 3.125e-05, + "input_cost_per_audio_per_second_above_128k_tokens": 6.25e-05, + "input_cost_per_character": 3.125e-07, + "input_cost_per_character_above_128k_tokens": 6.25e-07, + "input_cost_per_image": 0.00032875, + "input_cost_per_image_above_128k_tokens": 0.0006575, + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_above_128k_tokens": 2.5e-06, + "input_cost_per_video_per_second": 0.00032875, + "input_cost_per_video_per_second_above_128k_tokens": 0.0006575, + "litellm_provider": "vertex_ai-language-models", + "max_input_tokens": 1000000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_character": 1.25e-06, + "output_cost_per_character_above_128k_tokens": 2.5e-06, + "output_cost_per_token": 5e-06, + "output_cost_per_token_above_128k_tokens": 1e-05, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gemini-1.5-pro-002": { + "deprecation_date": "2025-09-24", + "input_cost_per_audio_per_second": 3.125e-05, + "input_cost_per_audio_per_second_above_128k_tokens": 6.25e-05, + "input_cost_per_character": 3.125e-07, + "input_cost_per_character_above_128k_tokens": 6.25e-07, + "input_cost_per_image": 0.00032875, + "input_cost_per_image_above_128k_tokens": 0.0006575, + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_above_128k_tokens": 2.5e-06, + "input_cost_per_video_per_second": 0.00032875, + "input_cost_per_video_per_second_above_128k_tokens": 0.0006575, + "litellm_provider": "vertex_ai-language-models", + "max_input_tokens": 2097152, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_character": 1.25e-06, + "output_cost_per_character_above_128k_tokens": 2.5e-06, + "output_cost_per_token": 5e-06, + "output_cost_per_token_above_128k_tokens": 1e-05, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-1.5-pro", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gemini-1.5-pro-preview-0215": { + "input_cost_per_audio_per_second": 3.125e-05, + "input_cost_per_audio_per_second_above_128k_tokens": 6.25e-05, + "input_cost_per_character": 3.125e-07, + "input_cost_per_character_above_128k_tokens": 6.25e-07, + "input_cost_per_image": 0.00032875, + "input_cost_per_image_above_128k_tokens": 0.0006575, + "input_cost_per_token": 7.8125e-08, + "input_cost_per_token_above_128k_tokens": 1.5625e-07, + "input_cost_per_video_per_second": 0.00032875, + "input_cost_per_video_per_second_above_128k_tokens": 0.0006575, + "litellm_provider": "vertex_ai-language-models", + "max_input_tokens": 1000000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_character": 1.25e-06, + "output_cost_per_character_above_128k_tokens": 2.5e-06, + "output_cost_per_token": 3.125e-07, + "output_cost_per_token_above_128k_tokens": 6.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gemini-1.5-pro-preview-0409": { + "input_cost_per_audio_per_second": 3.125e-05, + "input_cost_per_audio_per_second_above_128k_tokens": 6.25e-05, + "input_cost_per_character": 3.125e-07, + "input_cost_per_character_above_128k_tokens": 6.25e-07, + "input_cost_per_image": 0.00032875, + "input_cost_per_image_above_128k_tokens": 0.0006575, + "input_cost_per_token": 7.8125e-08, + "input_cost_per_token_above_128k_tokens": 1.5625e-07, + "input_cost_per_video_per_second": 0.00032875, + "input_cost_per_video_per_second_above_128k_tokens": 0.0006575, + "litellm_provider": "vertex_ai-language-models", + "max_input_tokens": 1000000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_character": 1.25e-06, + "output_cost_per_character_above_128k_tokens": 2.5e-06, + "output_cost_per_token": 3.125e-07, + "output_cost_per_token_above_128k_tokens": 6.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "gemini-1.5-pro-preview-0514": { + "input_cost_per_audio_per_second": 3.125e-05, + "input_cost_per_audio_per_second_above_128k_tokens": 6.25e-05, + "input_cost_per_character": 3.125e-07, + "input_cost_per_character_above_128k_tokens": 6.25e-07, + "input_cost_per_image": 0.00032875, + "input_cost_per_image_above_128k_tokens": 0.0006575, + "input_cost_per_token": 7.8125e-08, + "input_cost_per_token_above_128k_tokens": 1.5625e-07, + "input_cost_per_video_per_second": 0.00032875, + "input_cost_per_video_per_second_above_128k_tokens": 0.0006575, + "litellm_provider": "vertex_ai-language-models", + "max_input_tokens": 1000000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_character": 1.25e-06, + "output_cost_per_character_above_128k_tokens": 2.5e-06, + "output_cost_per_token": 3.125e-07, + "output_cost_per_token_above_128k_tokens": 6.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gemini-2.0-flash": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_audio_token": 7e-07, + "input_cost_per_token": 1e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 4e-07, + "source": "https://ai.google.dev/pricing#2_0flash", + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-2.0-flash-001": { + "cache_read_input_token_cost": 3.75e-08, + "deprecation_date": "2026-02-05", + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 1.5e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 6e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-2.0-flash-exp": { + "cache_read_input_token_cost": 3.75e-08, + "input_cost_per_audio_per_second": 0, + "input_cost_per_audio_per_second_above_128k_tokens": 0, + "input_cost_per_character": 0, + "input_cost_per_character_above_128k_tokens": 0, + "input_cost_per_image": 0, + "input_cost_per_image_above_128k_tokens": 0, + "input_cost_per_token": 1.5e-07, + "input_cost_per_token_above_128k_tokens": 0, + "input_cost_per_video_per_second": 0, + "input_cost_per_video_per_second_above_128k_tokens": 0, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_character": 0, + "output_cost_per_character_above_128k_tokens": 0, + "output_cost_per_token": 6e-07, + "output_cost_per_token_above_128k_tokens": 0, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-2.0-flash-lite": { + "cache_read_input_token_cost": 1.875e-08, + "input_cost_per_audio_token": 7.5e-08, + "input_cost_per_token": 7.5e-08, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 50, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 3e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-2.0-flash", + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-2.0-flash-lite-001": { + "cache_read_input_token_cost": 1.875e-08, + "deprecation_date": "2026-02-25", + "input_cost_per_audio_token": 7.5e-08, + "input_cost_per_token": 7.5e-08, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 50, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 3e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-2.0-flash", + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-2.0-flash-live-preview-04-09": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_audio_token": 3e-06, + "input_cost_per_image": 3e-06, + "input_cost_per_token": 5e-07, + "input_cost_per_video_per_second": 3e-06, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_audio_token": 1.2e-05, + "output_cost_per_token": 2e-06, + "rpm": 10, + "source": "https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/gemini#gemini-2-0-flash-live-preview-04-09", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "audio" + ], + "supports_audio_output": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 250000 + }, + "gemini-2.0-flash-preview-image-generation": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_audio_token": 7e-07, + "input_cost_per_token": 1e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 4e-07, + "source": "https://ai.google.dev/pricing#2_0flash", + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-2.0-flash-thinking-exp": { + "cache_read_input_token_cost": 0.0, + "input_cost_per_audio_per_second": 0, + "input_cost_per_audio_per_second_above_128k_tokens": 0, + "input_cost_per_character": 0, + "input_cost_per_character_above_128k_tokens": 0, + "input_cost_per_image": 0, + "input_cost_per_image_above_128k_tokens": 0, + "input_cost_per_token": 0, + "input_cost_per_token_above_128k_tokens": 0, + "input_cost_per_video_per_second": 0, + "input_cost_per_video_per_second_above_128k_tokens": 0, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_character": 0, + "output_cost_per_character_above_128k_tokens": 0, + "output_cost_per_token": 0, + "output_cost_per_token_above_128k_tokens": 0, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-2.0-flash", + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-2.0-flash-thinking-exp-01-21": { + "cache_read_input_token_cost": 0.0, + "input_cost_per_audio_per_second": 0, + "input_cost_per_audio_per_second_above_128k_tokens": 0, + "input_cost_per_character": 0, + "input_cost_per_character_above_128k_tokens": 0, + "input_cost_per_image": 0, + "input_cost_per_image_above_128k_tokens": 0, + "input_cost_per_token": 0, + "input_cost_per_token_above_128k_tokens": 0, + "input_cost_per_video_per_second": 0, + "input_cost_per_video_per_second_above_128k_tokens": 0, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65536, + "max_pdf_size_mb": 30, + "max_tokens": 65536, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_character": 0, + "output_cost_per_character_above_128k_tokens": 0, + "output_cost_per_token": 0, + "output_cost_per_token_above_128k_tokens": 0, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-2.0-flash", + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_audio_output": false, + "supports_function_calling": false, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": false, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-2.0-pro-exp-02-05": { + "cache_read_input_token_cost": 3.125e-07, + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_above_200k_tokens": 2.5e-06, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 2097152, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_above_200k_tokens": 1.5e-05, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_input": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_video_input": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-2.5-flash": { + "cache_read_input_token_cost": 3e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 3e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 2.5e-06, + "output_cost_per_token": 2.5e-06, + "source": "https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash-preview", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-2.5-flash-image": { + "cache_read_input_token_cost": 3e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 3e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "max_pdf_size_mb": 30, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "image_generation", + "output_cost_per_image": 0.039, + "output_cost_per_reasoning_token": 2.5e-06, + "output_cost_per_token": 2.5e-06, + "rpm": 100000, + "source": "https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-flash-image", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": false, + "tpm": 8000000 + }, + "gemini-2.5-flash-image-preview": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 3e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "image_generation", + "output_cost_per_image": 0.039, + "output_cost_per_reasoning_token": 3e-05, + "output_cost_per_token": 3e-05, + "rpm": 100000, + "source": "https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash-preview", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 8000000 + }, + "gemini-3-pro-image-preview": { + "input_cost_per_image": 0.0011, + "input_cost_per_token": 2e-06, + "input_cost_per_token_batches": 1e-06, + "litellm_provider": "vertex_ai-language-models", + "max_input_tokens": 65536, + "max_output_tokens": 32768, + "max_tokens": 65536, + "mode": "image_generation", + "output_cost_per_image": 0.134, + "output_cost_per_image_token": 1.2e-04, + "output_cost_per_token": 1.2e-05, + "output_cost_per_token_batches": 6e-06, + "source": "https://ai.google.dev/gemini-api/docs/pricing", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_function_calling": false, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-2.5-flash-lite": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_audio_token": 5e-07, + "input_cost_per_token": 1e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 4e-07, + "output_cost_per_token": 4e-07, + "source": "https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash-preview", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-2.5-flash-lite-preview-09-2025": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_audio_token": 3e-07, + "input_cost_per_token": 1e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 4e-07, + "output_cost_per_token": 4e-07, + "source": "https://developers.googleblog.com/en/continuing-to-bring-you-our-latest-models-with-an-improved-gemini-2-5-flash-and-flash-lite-release/", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-2.5-flash-preview-09-2025": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 3e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 2.5e-06, + "output_cost_per_token": 2.5e-06, + "source": "https://developers.googleblog.com/en/continuing-to-bring-you-our-latest-models-with-an-improved-gemini-2-5-flash-and-flash-lite-release/", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-live-2.5-flash-preview-native-audio-09-2025": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_audio_token": 3e-06, + "input_cost_per_token": 3e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_audio_token": 1.2e-05, + "output_cost_per_token": 2e-06, + "source": "https://ai.google.dev/gemini-api/docs/pricing", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "audio" + ], + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini/gemini-live-2.5-flash-preview-native-audio-09-2025": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_audio_token": 3e-06, + "input_cost_per_token": 3e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_audio_token": 1.2e-05, + "output_cost_per_token": 2e-06, + "rpm": 100000, + "source": "https://ai.google.dev/gemini-api/docs/pricing", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "audio" + ], + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 8000000 + }, + "gemini-2.5-flash-lite-preview-06-17": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_audio_token": 5e-07, + "input_cost_per_token": 1e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 4e-07, + "output_cost_per_token": 4e-07, + "source": "https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash-preview", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-2.5-flash-preview-04-17": { + "cache_read_input_token_cost": 3.75e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 1.5e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 3.5e-06, + "output_cost_per_token": 6e-07, + "source": "https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash-preview", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-2.5-flash-preview-05-20": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 3e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 2.5e-06, + "output_cost_per_token": 2.5e-06, + "source": "https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash-preview", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-2.5-pro": { + "cache_read_input_token_cost": 1.25e-07, + "cache_creation_input_token_cost_above_200k_tokens": 2.5e-07, + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_above_200k_tokens": 2.5e-06, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_above_200k_tokens": 1.5e-05, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_input": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_video_input": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-3-pro-preview": { + "cache_read_input_token_cost": 2e-07, + "cache_read_input_token_cost_above_200k_tokens": 4e-07, + "cache_creation_input_token_cost_above_200k_tokens": 2.5e-07, + "input_cost_per_token": 2e-06, + "input_cost_per_token_above_200k_tokens": 4e-06, + "input_cost_per_token_batches": 1e-06, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 1.2e-05, + "output_cost_per_token_above_200k_tokens": 1.8e-05, + "output_cost_per_token_batches": 6e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_input": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_video_input": true, + "supports_vision": true, + "supports_web_search": true + }, + "vertex_ai/gemini-3-pro-preview": { + "cache_read_input_token_cost": 2e-07, + "cache_read_input_token_cost_above_200k_tokens": 4e-07, + "cache_creation_input_token_cost_above_200k_tokens": 2.5e-07, + "input_cost_per_token": 2e-06, + "input_cost_per_token_above_200k_tokens": 4e-06, + "input_cost_per_token_batches": 1e-06, + "litellm_provider": "vertex_ai", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 1.2e-05, + "output_cost_per_token_above_200k_tokens": 1.8e-05, + "output_cost_per_token_batches": 6e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_input": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_video_input": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-2.5-pro-exp-03-25": { + "cache_read_input_token_cost": 3.125e-07, + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_above_200k_tokens": 2.5e-06, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_above_200k_tokens": 1.5e-05, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_input": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_video_input": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-2.5-pro-preview-03-25": { + "cache_read_input_token_cost": 3.125e-07, + "input_cost_per_audio_token": 1.25e-06, + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_above_200k_tokens": 2.5e-06, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_above_200k_tokens": 1.5e-05, + "source": "https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash-preview", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-2.5-pro-preview-05-06": { + "cache_read_input_token_cost": 3.125e-07, + "input_cost_per_audio_token": 1.25e-06, + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_above_200k_tokens": 2.5e-06, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_above_200k_tokens": 1.5e-05, + "source": "https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash-preview", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supported_regions": [ + "global" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-2.5-pro-preview-06-05": { + "cache_read_input_token_cost": 3.125e-07, + "input_cost_per_audio_token": 1.25e-06, + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_above_200k_tokens": 2.5e-06, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_above_200k_tokens": 1.5e-05, + "source": "https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash-preview", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-2.5-pro-preview-tts": { + "cache_read_input_token_cost": 3.125e-07, + "input_cost_per_audio_token": 7e-07, + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_above_200k_tokens": 2.5e-06, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_above_200k_tokens": 1.5e-05, + "source": "https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-pro-preview", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "audio" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini-embedding-001": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "vertex_ai-embedding-models", + "max_input_tokens": 2048, + "max_tokens": 2048, + "mode": "embedding", + "output_cost_per_token": 0, + "output_vector_size": 3072, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models" + }, + "gemini-flash-experimental": { + "input_cost_per_character": 0, + "input_cost_per_token": 0, + "litellm_provider": "vertex_ai-language-models", + "max_input_tokens": 1000000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_character": 0, + "output_cost_per_token": 0, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/gemini-experimental", + "supports_function_calling": false, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "gemini-pro": { + "input_cost_per_character": 1.25e-07, + "input_cost_per_image": 0.0025, + "input_cost_per_token": 5e-07, + "input_cost_per_video_per_second": 0.002, + "litellm_provider": "vertex_ai-language-models", + "max_input_tokens": 32760, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_character": 3.75e-07, + "output_cost_per_token": 1.5e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "gemini-pro-experimental": { + "input_cost_per_character": 0, + "input_cost_per_token": 0, + "litellm_provider": "vertex_ai-language-models", + "max_input_tokens": 1000000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_character": 0, + "output_cost_per_token": 0, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/gemini-experimental", + "supports_function_calling": false, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "gemini-pro-vision": { + "input_cost_per_image": 0.0025, + "input_cost_per_token": 5e-07, + "litellm_provider": "vertex_ai-vision-models", + "max_images_per_prompt": 16, + "max_input_tokens": 16384, + "max_output_tokens": 2048, + "max_tokens": 2048, + "max_video_length": 2, + "max_videos_per_prompt": 1, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gemini/gemini-embedding-001": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "gemini", + "max_input_tokens": 2048, + "max_tokens": 2048, + "mode": "embedding", + "output_cost_per_token": 0, + "output_vector_size": 3072, + "rpm": 10000, + "source": "https://ai.google.dev/gemini-api/docs/embeddings#model-versions", + "tpm": 10000000 + }, + "gemini/gemini-1.5-flash": { + "input_cost_per_token": 7.5e-08, + "input_cost_per_token_above_128k_tokens": 1.5e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 3e-07, + "output_cost_per_token_above_128k_tokens": 6e-07, + "rpm": 2000, + "source": "https://ai.google.dev/pricing", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "tpm": 4000000 + }, + "gemini/gemini-1.5-flash-001": { + "cache_creation_input_token_cost": 1e-06, + "cache_read_input_token_cost": 1.875e-08, + "deprecation_date": "2025-05-24", + "input_cost_per_token": 7.5e-08, + "input_cost_per_token_above_128k_tokens": 1.5e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 3e-07, + "output_cost_per_token_above_128k_tokens": 6e-07, + "rpm": 2000, + "source": "https://ai.google.dev/pricing", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "tpm": 4000000 + }, + "gemini/gemini-1.5-flash-002": { + "cache_creation_input_token_cost": 1e-06, + "cache_read_input_token_cost": 1.875e-08, + "deprecation_date": "2025-09-24", + "input_cost_per_token": 7.5e-08, + "input_cost_per_token_above_128k_tokens": 1.5e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 3e-07, + "output_cost_per_token_above_128k_tokens": 6e-07, + "rpm": 2000, + "source": "https://ai.google.dev/pricing", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "tpm": 4000000 + }, + "gemini/gemini-1.5-flash-8b": { + "input_cost_per_token": 0, + "input_cost_per_token_above_128k_tokens": 0, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 0, + "output_cost_per_token_above_128k_tokens": 0, + "rpm": 4000, + "source": "https://ai.google.dev/pricing", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "tpm": 4000000 + }, + "gemini/gemini-1.5-flash-8b-exp-0827": { + "input_cost_per_token": 0, + "input_cost_per_token_above_128k_tokens": 0, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1000000, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 0, + "output_cost_per_token_above_128k_tokens": 0, + "rpm": 4000, + "source": "https://ai.google.dev/pricing", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "tpm": 4000000 + }, + "gemini/gemini-1.5-flash-8b-exp-0924": { + "input_cost_per_token": 0, + "input_cost_per_token_above_128k_tokens": 0, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 0, + "output_cost_per_token_above_128k_tokens": 0, + "rpm": 4000, + "source": "https://ai.google.dev/pricing", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "tpm": 4000000 + }, + "gemini/gemini-1.5-flash-exp-0827": { + "input_cost_per_token": 0, + "input_cost_per_token_above_128k_tokens": 0, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 0, + "output_cost_per_token_above_128k_tokens": 0, + "rpm": 2000, + "source": "https://ai.google.dev/pricing", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "tpm": 4000000 + }, + "gemini/gemini-1.5-flash-latest": { + "input_cost_per_token": 7.5e-08, + "input_cost_per_token_above_128k_tokens": 1.5e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 3e-07, + "output_cost_per_token_above_128k_tokens": 6e-07, + "rpm": 2000, + "source": "https://ai.google.dev/pricing", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "tpm": 4000000 + }, + "gemini/gemini-1.5-pro": { + "input_cost_per_token": 3.5e-06, + "input_cost_per_token_above_128k_tokens": 7e-06, + "litellm_provider": "gemini", + "max_input_tokens": 2097152, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.05e-05, + "output_cost_per_token_above_128k_tokens": 2.1e-05, + "rpm": 1000, + "source": "https://ai.google.dev/pricing", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "tpm": 4000000 + }, + "gemini/gemini-1.5-pro-001": { + "deprecation_date": "2025-05-24", + "input_cost_per_token": 3.5e-06, + "input_cost_per_token_above_128k_tokens": 7e-06, + "litellm_provider": "gemini", + "max_input_tokens": 2097152, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.05e-05, + "output_cost_per_token_above_128k_tokens": 2.1e-05, + "rpm": 1000, + "source": "https://ai.google.dev/pricing", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "tpm": 4000000 + }, + "gemini/gemini-1.5-pro-002": { + "deprecation_date": "2025-09-24", + "input_cost_per_token": 3.5e-06, + "input_cost_per_token_above_128k_tokens": 7e-06, + "litellm_provider": "gemini", + "max_input_tokens": 2097152, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.05e-05, + "output_cost_per_token_above_128k_tokens": 2.1e-05, + "rpm": 1000, + "source": "https://ai.google.dev/pricing", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "tpm": 4000000 + }, + "gemini/gemini-1.5-pro-exp-0801": { + "input_cost_per_token": 3.5e-06, + "input_cost_per_token_above_128k_tokens": 7e-06, + "litellm_provider": "gemini", + "max_input_tokens": 2097152, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.05e-05, + "output_cost_per_token_above_128k_tokens": 2.1e-05, + "rpm": 1000, + "source": "https://ai.google.dev/pricing", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "tpm": 4000000 + }, + "gemini/gemini-1.5-pro-exp-0827": { + "input_cost_per_token": 0, + "input_cost_per_token_above_128k_tokens": 0, + "litellm_provider": "gemini", + "max_input_tokens": 2097152, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 0, + "output_cost_per_token_above_128k_tokens": 0, + "rpm": 1000, + "source": "https://ai.google.dev/pricing", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "tpm": 4000000 + }, + "gemini/gemini-1.5-pro-latest": { + "input_cost_per_token": 3.5e-06, + "input_cost_per_token_above_128k_tokens": 7e-06, + "litellm_provider": "gemini", + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.05e-06, + "output_cost_per_token_above_128k_tokens": 2.1e-05, + "rpm": 1000, + "source": "https://ai.google.dev/pricing", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "tpm": 4000000 + }, + "gemini/gemini-2.0-flash": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_audio_token": 7e-07, + "input_cost_per_token": 1e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 4e-07, + "rpm": 10000, + "source": "https://ai.google.dev/pricing#2_0flash", + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 10000000 + }, + "gemini/gemini-2.0-flash-001": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_audio_token": 7e-07, + "input_cost_per_token": 1e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 4e-07, + "rpm": 10000, + "source": "https://ai.google.dev/pricing#2_0flash", + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 10000000 + }, + "gemini/gemini-2.0-flash-exp": { + "cache_read_input_token_cost": 0.0, + "input_cost_per_audio_per_second": 0, + "input_cost_per_audio_per_second_above_128k_tokens": 0, + "input_cost_per_character": 0, + "input_cost_per_character_above_128k_tokens": 0, + "input_cost_per_image": 0, + "input_cost_per_image_above_128k_tokens": 0, + "input_cost_per_token": 0, + "input_cost_per_token_above_128k_tokens": 0, + "input_cost_per_video_per_second": 0, + "input_cost_per_video_per_second_above_128k_tokens": 0, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_character": 0, + "output_cost_per_character_above_128k_tokens": 0, + "output_cost_per_token": 0, + "output_cost_per_token_above_128k_tokens": 0, + "rpm": 10, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-2.0-flash", + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_audio_output": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 4000000 + }, + "gemini/gemini-2.0-flash-lite": { + "cache_read_input_token_cost": 1.875e-08, + "input_cost_per_audio_token": 7.5e-08, + "input_cost_per_token": 7.5e-08, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 50, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 3e-07, + "rpm": 4000, + "source": "https://ai.google.dev/gemini-api/docs/pricing#gemini-2.0-flash-lite", + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 4000000 + }, + "gemini/gemini-2.0-flash-lite-preview-02-05": { + "cache_read_input_token_cost": 1.875e-08, + "input_cost_per_audio_token": 7.5e-08, + "input_cost_per_token": 7.5e-08, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 3e-07, + "rpm": 60000, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-2.0-flash-lite", + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 10000000 + }, + "gemini/gemini-2.0-flash-live-001": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_audio_token": 2.1e-06, + "input_cost_per_image": 2.1e-06, + "input_cost_per_token": 3.5e-07, + "input_cost_per_video_per_second": 2.1e-06, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_audio_token": 8.5e-06, + "output_cost_per_token": 1.5e-06, + "rpm": 10, + "source": "https://ai.google.dev/gemini-api/docs/models#gemini-2-0-flash-live-001", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "audio" + ], + "supports_audio_output": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 250000 + }, + "gemini/gemini-2.0-flash-preview-image-generation": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_audio_token": 7e-07, + "input_cost_per_token": 1e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 4e-07, + "rpm": 10000, + "source": "https://ai.google.dev/pricing#2_0flash", + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 10000000 + }, + "gemini/gemini-2.0-flash-thinking-exp": { + "cache_read_input_token_cost": 0.0, + "input_cost_per_audio_per_second": 0, + "input_cost_per_audio_per_second_above_128k_tokens": 0, + "input_cost_per_character": 0, + "input_cost_per_character_above_128k_tokens": 0, + "input_cost_per_image": 0, + "input_cost_per_image_above_128k_tokens": 0, + "input_cost_per_token": 0, + "input_cost_per_token_above_128k_tokens": 0, + "input_cost_per_video_per_second": 0, + "input_cost_per_video_per_second_above_128k_tokens": 0, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65536, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_character": 0, + "output_cost_per_character_above_128k_tokens": 0, + "output_cost_per_token": 0, + "output_cost_per_token_above_128k_tokens": 0, + "rpm": 10, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-2.0-flash", + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_audio_output": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 4000000 + }, + "gemini/gemini-2.0-flash-thinking-exp-01-21": { + "cache_read_input_token_cost": 0.0, + "input_cost_per_audio_per_second": 0, + "input_cost_per_audio_per_second_above_128k_tokens": 0, + "input_cost_per_character": 0, + "input_cost_per_character_above_128k_tokens": 0, + "input_cost_per_image": 0, + "input_cost_per_image_above_128k_tokens": 0, + "input_cost_per_token": 0, + "input_cost_per_token_above_128k_tokens": 0, + "input_cost_per_video_per_second": 0, + "input_cost_per_video_per_second_above_128k_tokens": 0, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65536, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_character": 0, + "output_cost_per_character_above_128k_tokens": 0, + "output_cost_per_token": 0, + "output_cost_per_token_above_128k_tokens": 0, + "rpm": 10, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-2.0-flash", + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_audio_output": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 4000000 + }, + "gemini/gemini-2.0-pro-exp-02-05": { + "cache_read_input_token_cost": 0.0, + "input_cost_per_audio_per_second": 0, + "input_cost_per_audio_per_second_above_128k_tokens": 0, + "input_cost_per_character": 0, + "input_cost_per_character_above_128k_tokens": 0, + "input_cost_per_image": 0, + "input_cost_per_image_above_128k_tokens": 0, + "input_cost_per_token": 0, + "input_cost_per_token_above_128k_tokens": 0, + "input_cost_per_video_per_second": 0, + "input_cost_per_video_per_second_above_128k_tokens": 0, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 2097152, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_character": 0, + "output_cost_per_character_above_128k_tokens": 0, + "output_cost_per_token": 0, + "output_cost_per_token_above_128k_tokens": 0, + "rpm": 2, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", + "supports_audio_input": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_video_input": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 1000000 + }, + "gemini/gemini-2.5-flash": { + "cache_read_input_token_cost": 3e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 3e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 2.5e-06, + "output_cost_per_token": 2.5e-06, + "rpm": 100000, + "source": "https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash-preview", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 8000000 + }, + "gemini/gemini-2.5-flash-image": { + "cache_read_input_token_cost": 3e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 3e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "supports_reasoning": false, + "max_images_per_prompt": 3000, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "max_pdf_size_mb": 30, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "image_generation", + "output_cost_per_image": 0.039, + "output_cost_per_reasoning_token": 2.5e-06, + "output_cost_per_token": 2.5e-06, + "rpm": 100000, + "source": "https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-flash-image", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 8000000 + }, + "gemini/gemini-2.5-flash-image-preview": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 3e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "image_generation", + "output_cost_per_image": 0.039, + "output_cost_per_reasoning_token": 3e-05, + "output_cost_per_token": 3e-05, + "rpm": 100000, + "source": "https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash-preview", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 8000000 + }, + "gemini/gemini-3-pro-image-preview": { + "input_cost_per_image": 0.0011, + "input_cost_per_token": 2e-06, + "input_cost_per_token_batches": 1e-06, + "litellm_provider": "gemini", + "max_input_tokens": 65536, + "max_output_tokens": 32768, + "max_tokens": 65536, + "mode": "image_generation", + "output_cost_per_image": 0.134, + "output_cost_per_image_token": 1.2e-04, + "output_cost_per_token": 1.2e-05, + "rpm": 1000, + "tpm": 4000000, + "output_cost_per_token_batches": 6e-06, + "source": "https://ai.google.dev/gemini-api/docs/pricing", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_function_calling": false, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini/gemini-2.5-flash-lite": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_audio_token": 5e-07, + "input_cost_per_token": 1e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 4e-07, + "output_cost_per_token": 4e-07, + "rpm": 15, + "source": "https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash-lite", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 250000 + }, + "gemini/gemini-2.5-flash-lite-preview-09-2025": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_audio_token": 3e-07, + "input_cost_per_token": 1e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 4e-07, + "output_cost_per_token": 4e-07, + "rpm": 15, + "source": "https://developers.googleblog.com/en/continuing-to-bring-you-our-latest-models-with-an-improved-gemini-2-5-flash-and-flash-lite-release/", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 250000 + }, + "gemini/gemini-2.5-flash-preview-09-2025": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 3e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 2.5e-06, + "output_cost_per_token": 2.5e-06, + "rpm": 15, + "source": "https://developers.googleblog.com/en/continuing-to-bring-you-our-latest-models-with-an-improved-gemini-2-5-flash-and-flash-lite-release/", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 250000 + }, + "gemini/gemini-flash-latest": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 3e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 2.5e-06, + "output_cost_per_token": 2.5e-06, + "rpm": 15, + "source": "https://developers.googleblog.com/en/continuing-to-bring-you-our-latest-models-with-an-improved-gemini-2-5-flash-and-flash-lite-release/", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 250000 + }, + "gemini/gemini-flash-lite-latest": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_audio_token": 3e-07, + "input_cost_per_token": 1e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 4e-07, + "output_cost_per_token": 4e-07, + "rpm": 15, + "source": "https://developers.googleblog.com/en/continuing-to-bring-you-our-latest-models-with-an-improved-gemini-2-5-flash-and-flash-lite-release/", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 250000 + }, + "gemini/gemini-2.5-flash-lite-preview-06-17": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_audio_token": 5e-07, + "input_cost_per_token": 1e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 4e-07, + "output_cost_per_token": 4e-07, + "rpm": 15, + "source": "https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash-lite", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 250000 + }, + "gemini/gemini-2.5-flash-preview-04-17": { + "cache_read_input_token_cost": 3.75e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 1.5e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 3.5e-06, + "output_cost_per_token": 6e-07, + "rpm": 10, + "source": "https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash-preview", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 250000 + }, + "gemini/gemini-2.5-flash-preview-05-20": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 3e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 2.5e-06, + "output_cost_per_token": 2.5e-06, + "rpm": 10, + "source": "https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash-preview", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 250000 + }, + "gemini/gemini-2.5-flash-preview-tts": { + "cache_read_input_token_cost": 3.75e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 1.5e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 3.5e-06, + "output_cost_per_token": 6e-07, + "rpm": 10, + "source": "https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash-preview", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions" + ], + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "audio" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 250000 + }, + "gemini/gemini-2.5-pro": { + "cache_read_input_token_cost": 3.125e-07, + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_above_200k_tokens": 2.5e-06, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_above_200k_tokens": 1.5e-05, + "rpm": 2000, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_input": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_video_input": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 800000 + }, + "gemini/gemini-2.5-computer-use-preview-10-2025": { + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_above_200k_tokens": 2.5e-06, + "litellm_provider": "gemini", + "max_images_per_prompt": 3000, + "max_input_tokens": 128000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_above_200k_tokens": 1.5e-05, + "rpm": 2000, + "source": "https://ai.google.dev/gemini-api/docs/computer-use", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_computer_use": true, + "supports_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "tpm": 800000 + }, + "gemini/gemini-3-pro-preview": { + "cache_read_input_token_cost": 2e-07, + "cache_read_input_token_cost_above_200k_tokens": 4e-07, + "input_cost_per_token": 2e-06, + "input_cost_per_token_above_200k_tokens": 4e-06, + "input_cost_per_token_batches": 1e-06, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 1.2e-05, + "output_cost_per_token_above_200k_tokens": 1.8e-05, + "output_cost_per_token_batches": 6e-06, + "rpm": 2000, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_input": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_video_input": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 800000 + }, + "gemini/gemini-3-flash-preview": { + "cache_read_input_token_cost": 5e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 5e-07, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 3e-06, + "output_cost_per_token": 3e-06, + "rpm": 2000, + "source": "https://ai.google.dev/pricing/gemini-3", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 800000 + }, + "gemini-3-flash-preview": { + "cache_read_input_token_cost": 5e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 5e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_reasoning_token": 3e-06, + "output_cost_per_token": 3e-06, + "source": "https://ai.google.dev/pricing/gemini-3", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true + }, + "gemini/gemini-2.5-pro-exp-03-25": { + "cache_read_input_token_cost": 0.0, + "input_cost_per_token": 0.0, + "input_cost_per_token_above_200k_tokens": 0.0, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 0.0, + "output_cost_per_token_above_200k_tokens": 0.0, + "rpm": 5, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_input": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_video_input": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 250000 + }, + "gemini/gemini-2.5-pro-preview-03-25": { + "cache_read_input_token_cost": 3.125e-07, + "input_cost_per_audio_token": 7e-07, + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_above_200k_tokens": 2.5e-06, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_above_200k_tokens": 1.5e-05, + "rpm": 10000, + "source": "https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-pro-preview", + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 10000000 + }, + "gemini/gemini-2.5-pro-preview-05-06": { + "cache_read_input_token_cost": 3.125e-07, + "input_cost_per_audio_token": 7e-07, + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_above_200k_tokens": 2.5e-06, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_above_200k_tokens": 1.5e-05, + "rpm": 10000, + "source": "https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-pro-preview", + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 10000000 + }, + "gemini/gemini-2.5-pro-preview-06-05": { + "cache_read_input_token_cost": 3.125e-07, + "input_cost_per_audio_token": 7e-07, + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_above_200k_tokens": 2.5e-06, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_above_200k_tokens": 1.5e-05, + "rpm": 10000, + "source": "https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-pro-preview", + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 10000000 + }, + "gemini/gemini-2.5-pro-preview-tts": { + "cache_read_input_token_cost": 3.125e-07, + "input_cost_per_audio_token": 7e-07, + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_above_200k_tokens": 2.5e-06, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_above_200k_tokens": 1.5e-05, + "rpm": 10000, + "source": "https://ai.google.dev/gemini-api/docs/pricing#gemini-2.5-pro-preview", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "audio" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true, + "tpm": 10000000 + }, + "gemini/gemini-exp-1114": { + "input_cost_per_token": 0, + "input_cost_per_token_above_128k_tokens": 0, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "metadata": { + "notes": "Rate limits not documented for gemini-exp-1114. Assuming same as gemini-1.5-pro.", + "supports_tool_choice": true + }, + "mode": "chat", + "output_cost_per_token": 0, + "output_cost_per_token_above_128k_tokens": 0, + "rpm": 1000, + "source": "https://ai.google.dev/pricing", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "tpm": 4000000 + }, + "gemini/gemini-exp-1206": { + "input_cost_per_token": 0, + "input_cost_per_token_above_128k_tokens": 0, + "litellm_provider": "gemini", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 2097152, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "metadata": { + "notes": "Rate limits not documented for gemini-exp-1206. Assuming same as gemini-1.5-pro.", + "supports_tool_choice": true + }, + "mode": "chat", + "output_cost_per_token": 0, + "output_cost_per_token_above_128k_tokens": 0, + "rpm": 1000, + "source": "https://ai.google.dev/pricing", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "tpm": 4000000 + }, + "gemini/gemini-gemma-2-27b-it": { + "input_cost_per_token": 3.5e-07, + "litellm_provider": "gemini", + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.05e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gemini/gemini-gemma-2-9b-it": { + "input_cost_per_token": 3.5e-07, + "litellm_provider": "gemini", + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.05e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gemini/gemini-pro": { + "input_cost_per_token": 3.5e-07, + "input_cost_per_token_above_128k_tokens": 7e-07, + "litellm_provider": "gemini", + "max_input_tokens": 32760, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.05e-06, + "output_cost_per_token_above_128k_tokens": 2.1e-06, + "rpd": 30000, + "rpm": 360, + "source": "https://ai.google.dev/gemini-api/docs/models/gemini", + "supports_function_calling": true, + "supports_tool_choice": true, + "tpm": 120000 + }, + "gemini/gemini-pro-vision": { + "input_cost_per_token": 3.5e-07, + "input_cost_per_token_above_128k_tokens": 7e-07, + "litellm_provider": "gemini", + "max_input_tokens": 30720, + "max_output_tokens": 2048, + "max_tokens": 2048, + "mode": "chat", + "output_cost_per_token": 1.05e-06, + "output_cost_per_token_above_128k_tokens": 2.1e-06, + "rpd": 30000, + "rpm": 360, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true, + "tpm": 120000 + }, + "gemini/gemma-3-27b-it": { + "input_cost_per_audio_per_second": 0, + "input_cost_per_audio_per_second_above_128k_tokens": 0, + "input_cost_per_character": 0, + "input_cost_per_character_above_128k_tokens": 0, + "input_cost_per_image": 0, + "input_cost_per_image_above_128k_tokens": 0, + "input_cost_per_token": 0, + "input_cost_per_token_above_128k_tokens": 0, + "input_cost_per_video_per_second": 0, + "input_cost_per_video_per_second_above_128k_tokens": 0, + "litellm_provider": "gemini", + "max_input_tokens": 131072, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_character": 0, + "output_cost_per_character_above_128k_tokens": 0, + "output_cost_per_token": 0, + "output_cost_per_token_above_128k_tokens": 0, + "source": "https://aistudio.google.com", + "supports_audio_output": false, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "gemini/imagen-3.0-fast-generate-001": { + "litellm_provider": "gemini", + "mode": "image_generation", + "output_cost_per_image": 0.02, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing" + }, + "gemini/imagen-3.0-generate-001": { + "litellm_provider": "gemini", + "mode": "image_generation", + "output_cost_per_image": 0.04, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing" + }, + "gemini/imagen-3.0-generate-002": { + "litellm_provider": "gemini", + "mode": "image_generation", + "output_cost_per_image": 0.04, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing" + }, + "gemini/imagen-4.0-fast-generate-001": { + "litellm_provider": "gemini", + "mode": "image_generation", + "output_cost_per_image": 0.02, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing" + }, + "gemini/imagen-4.0-generate-001": { + "litellm_provider": "gemini", + "mode": "image_generation", + "output_cost_per_image": 0.04, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing" + }, + "gemini/imagen-4.0-ultra-generate-001": { + "litellm_provider": "gemini", + "mode": "image_generation", + "output_cost_per_image": 0.06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing" + }, + "gemini/learnlm-1.5-pro-experimental": { + "input_cost_per_audio_per_second": 0, + "input_cost_per_audio_per_second_above_128k_tokens": 0, + "input_cost_per_character": 0, + "input_cost_per_character_above_128k_tokens": 0, + "input_cost_per_image": 0, + "input_cost_per_image_above_128k_tokens": 0, + "input_cost_per_token": 0, + "input_cost_per_token_above_128k_tokens": 0, + "input_cost_per_video_per_second": 0, + "input_cost_per_video_per_second_above_128k_tokens": 0, + "litellm_provider": "gemini", + "max_input_tokens": 32767, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_character": 0, + "output_cost_per_character_above_128k_tokens": 0, + "output_cost_per_token": 0, + "output_cost_per_token_above_128k_tokens": 0, + "source": "https://aistudio.google.com", + "supports_audio_output": false, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gemini/veo-2.0-generate-001": { + "litellm_provider": "gemini", + "max_input_tokens": 1024, + "max_tokens": 1024, + "mode": "video_generation", + "output_cost_per_second": 0.35, + "source": "https://ai.google.dev/gemini-api/docs/video", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "video" + ] + }, + "gemini/veo-3.0-fast-generate-preview": { + "litellm_provider": "gemini", + "max_input_tokens": 1024, + "max_tokens": 1024, + "mode": "video_generation", + "output_cost_per_second": 0.4, + "source": "https://ai.google.dev/gemini-api/docs/video", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "video" + ] + }, + "gemini/veo-3.0-generate-preview": { + "litellm_provider": "gemini", + "max_input_tokens": 1024, + "max_tokens": 1024, + "mode": "video_generation", + "output_cost_per_second": 0.75, + "source": "https://ai.google.dev/gemini-api/docs/video", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "video" + ] + }, + "gemini/veo-3.1-fast-generate-preview": { + "litellm_provider": "gemini", + "max_input_tokens": 1024, + "max_tokens": 1024, + "mode": "video_generation", + "output_cost_per_second": 0.15, + "source": "https://ai.google.dev/gemini-api/docs/video", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "video" + ] + }, + "gemini/veo-3.1-generate-preview": { + "litellm_provider": "gemini", + "max_input_tokens": 1024, + "max_tokens": 1024, + "mode": "video_generation", + "output_cost_per_second": 0.40, + "source": "https://ai.google.dev/gemini-api/docs/video", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "video" + ] + }, + "github_copilot/claude-haiku-4.5": { + "litellm_provider": "github_copilot", + "max_input_tokens": 128000, + "max_output_tokens": 16000, + "max_tokens": 16000, + "mode": "chat", + "supported_endpoints": [ + "/chat/completions" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_vision": true + }, + "github_copilot/claude-opus-4.5": { + "litellm_provider": "github_copilot", + "max_input_tokens": 128000, + "max_output_tokens": 16000, + "max_tokens": 16000, + "mode": "chat", + "supported_endpoints": [ + "/chat/completions" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_vision": true + }, + "github_copilot/claude-opus-41": { + "litellm_provider": "github_copilot", + "max_input_tokens": 80000, + "max_output_tokens": 16000, + "max_tokens": 16000, + "mode": "chat", + "supported_endpoints": [ + "/chat/completions" + ], + "supports_vision": true + }, + "github_copilot/claude-sonnet-4": { + "litellm_provider": "github_copilot", + "max_input_tokens": 128000, + "max_output_tokens": 16000, + "max_tokens": 16000, + "mode": "chat", + "supported_endpoints": [ + "/chat/completions" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_vision": true + }, + "github_copilot/claude-sonnet-4.5": { + "litellm_provider": "github_copilot", + "max_input_tokens": 128000, + "max_output_tokens": 16000, + "max_tokens": 16000, + "mode": "chat", + "supported_endpoints": [ + "/chat/completions" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_vision": true + }, + "github_copilot/gemini-2.5-pro": { + "litellm_provider": "github_copilot", + "max_input_tokens": 128000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_vision": true + }, + "github_copilot/gemini-3-pro-preview": { + "litellm_provider": "github_copilot", + "max_input_tokens": 128000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_vision": true + }, + "github_copilot/gpt-3.5-turbo": { + "litellm_provider": "github_copilot", + "max_input_tokens": 16384, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "supports_function_calling": true + }, + "github_copilot/gpt-3.5-turbo-0613": { + "litellm_provider": "github_copilot", + "max_input_tokens": 16384, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "supports_function_calling": true + }, + "github_copilot/gpt-4": { + "litellm_provider": "github_copilot", + "max_input_tokens": 32768, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "supports_function_calling": true + }, + "github_copilot/gpt-4-0613": { + "litellm_provider": "github_copilot", + "max_input_tokens": 32768, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "supports_function_calling": true + }, + "github_copilot/gpt-4-o-preview": { + "litellm_provider": "github_copilot", + "max_input_tokens": 64000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true + }, + "github_copilot/gpt-4.1": { + "litellm_provider": "github_copilot", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_vision": true + }, + "github_copilot/gpt-4.1-2025-04-14": { + "litellm_provider": "github_copilot", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_vision": true + }, + "github_copilot/gpt-41-copilot": { + "litellm_provider": "github_copilot", + "mode": "completion" + }, + "github_copilot/gpt-4o": { + "litellm_provider": "github_copilot", + "max_input_tokens": 64000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_vision": true + }, + "github_copilot/gpt-4o-2024-05-13": { + "litellm_provider": "github_copilot", + "max_input_tokens": 64000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_vision": true + }, + "github_copilot/gpt-4o-2024-08-06": { + "litellm_provider": "github_copilot", + "max_input_tokens": 64000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true + }, + "github_copilot/gpt-4o-2024-11-20": { + "litellm_provider": "github_copilot", + "max_input_tokens": 64000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_vision": true + }, + "github_copilot/gpt-4o-mini": { + "litellm_provider": "github_copilot", + "max_input_tokens": 64000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true + }, + "github_copilot/gpt-4o-mini-2024-07-18": { + "litellm_provider": "github_copilot", + "max_input_tokens": 64000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true + }, + "github_copilot/gpt-5": { + "litellm_provider": "github_copilot", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "supported_endpoints": [ + "/chat/completions", + "/responses" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_vision": true + }, + "github_copilot/gpt-5-mini": { + "litellm_provider": "github_copilot", + "max_input_tokens": 128000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_vision": true + }, + "github_copilot/gpt-5.1": { + "litellm_provider": "github_copilot", + "max_input_tokens": 128000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "supported_endpoints": [ + "/chat/completions", + "/responses" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_vision": true + }, + "github_copilot/gpt-5.1-codex-max": { + "litellm_provider": "github_copilot", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "supported_endpoints": [ + "/responses" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_vision": true + }, + "github_copilot/gpt-5.2": { + "litellm_provider": "github_copilot", + "max_input_tokens": 128000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "supported_endpoints": [ + "/chat/completions", + "/responses" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_vision": true + }, + "github_copilot/text-embedding-3-small": { + "litellm_provider": "github_copilot", + "max_input_tokens": 8191, + "max_tokens": 8191, + "mode": "embedding" + }, + "github_copilot/text-embedding-3-small-inference": { + "litellm_provider": "github_copilot", + "max_input_tokens": 8191, + "max_tokens": 8191, + "mode": "embedding" + }, + "github_copilot/text-embedding-ada-002": { + "litellm_provider": "github_copilot", + "max_input_tokens": 8191, + "max_tokens": 8191, + "mode": "embedding" + }, + "google.gemma-3-12b-it": { + "input_cost_per_token": 9e-08, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2.9e-07, + "supports_system_messages": true, + "supports_vision": true + }, + "google.gemma-3-27b-it": { + "input_cost_per_token": 2.3e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 3.8e-07, + "supports_system_messages": true, + "supports_vision": true + }, + "google.gemma-3-4b-it": { + "input_cost_per_token": 4e-08, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 8e-08, + "supports_system_messages": true, + "supports_vision": true + }, + "google_pse/search": { + "input_cost_per_query": 0.005, + "litellm_provider": "google_pse", + "mode": "search" + }, + "global.anthropic.claude-sonnet-4-5-20250929-v1:0": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 346 + }, + "global.anthropic.claude-sonnet-4-20250514-v1:0": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "global.anthropic.claude-haiku-4-5-20251001-v1:0": { + "cache_creation_input_token_cost": 1.25e-06, + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 1e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 5e-06, + "source": "https://aws.amazon.com/about-aws/whats-new/2025/10/claude-4-5-haiku-anthropic-amazon-bedrock", + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 346 + }, + "global.amazon.nova-2-lite-v1:0": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_token": 3e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.5e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_video_input": true, + "supports_vision": true + }, + "gpt-3.5-turbo": { + "input_cost_per_token": 0.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 16385, + "max_output_tokens": 4096, + "max_tokens": 4097, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-3.5-turbo-0125": { + "input_cost_per_token": 5e-07, + "litellm_provider": "openai", + "max_input_tokens": 16385, + "max_output_tokens": 4096, + "max_tokens": 16385, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-3.5-turbo-0301": { + "input_cost_per_token": 1.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 4097, + "max_output_tokens": 4096, + "max_tokens": 4097, + "mode": "chat", + "output_cost_per_token": 2e-06, + "supports_prompt_caching": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-3.5-turbo-0613": { + "input_cost_per_token": 1.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 4097, + "max_output_tokens": 4096, + "max_tokens": 4097, + "mode": "chat", + "output_cost_per_token": 2e-06, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-3.5-turbo-1106": { + "deprecation_date": "2026-09-28", + "input_cost_per_token": 1e-06, + "litellm_provider": "openai", + "max_input_tokens": 16385, + "max_output_tokens": 4096, + "max_tokens": 16385, + "mode": "chat", + "output_cost_per_token": 2e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-3.5-turbo-16k": { + "input_cost_per_token": 3e-06, + "litellm_provider": "openai", + "max_input_tokens": 16385, + "max_output_tokens": 4096, + "max_tokens": 16385, + "mode": "chat", + "output_cost_per_token": 4e-06, + "supports_prompt_caching": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-3.5-turbo-16k-0613": { + "input_cost_per_token": 3e-06, + "litellm_provider": "openai", + "max_input_tokens": 16385, + "max_output_tokens": 4096, + "max_tokens": 16385, + "mode": "chat", + "output_cost_per_token": 4e-06, + "supports_prompt_caching": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-3.5-turbo-instruct": { + "input_cost_per_token": 1.5e-06, + "litellm_provider": "text-completion-openai", + "max_input_tokens": 8192, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "completion", + "output_cost_per_token": 2e-06 + }, + "gpt-3.5-turbo-instruct-0914": { + "input_cost_per_token": 1.5e-06, + "litellm_provider": "text-completion-openai", + "max_input_tokens": 8192, + "max_output_tokens": 4097, + "max_tokens": 4097, + "mode": "completion", + "output_cost_per_token": 2e-06 + }, + "gpt-4": { + "input_cost_per_token": 3e-05, + "litellm_provider": "openai", + "max_input_tokens": 8192, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 6e-05, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-4-0125-preview": { + "deprecation_date": "2026-03-26", + "input_cost_per_token": 1e-05, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 3e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-4-0314": { + "input_cost_per_token": 3e-05, + "litellm_provider": "openai", + "max_input_tokens": 8192, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 6e-05, + "supports_prompt_caching": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-4-0613": { + "deprecation_date": "2025-06-06", + "input_cost_per_token": 3e-05, + "litellm_provider": "openai", + "max_input_tokens": 8192, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 6e-05, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-4-1106-preview": { + "deprecation_date": "2026-03-26", + "input_cost_per_token": 1e-05, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 3e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-4-1106-vision-preview": { + "deprecation_date": "2024-12-06", + "input_cost_per_token": 1e-05, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 3e-05, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gpt-4-32k": { + "input_cost_per_token": 6e-05, + "litellm_provider": "openai", + "max_input_tokens": 32768, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 0.00012, + "supports_prompt_caching": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-4-32k-0314": { + "input_cost_per_token": 6e-05, + "litellm_provider": "openai", + "max_input_tokens": 32768, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 0.00012, + "supports_prompt_caching": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-4-32k-0613": { + "input_cost_per_token": 6e-05, + "litellm_provider": "openai", + "max_input_tokens": 32768, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 0.00012, + "supports_prompt_caching": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-4-turbo": { + "input_cost_per_token": 1e-05, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 3e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gpt-4-turbo-2024-04-09": { + "input_cost_per_token": 1e-05, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 3e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gpt-4-turbo-preview": { + "input_cost_per_token": 1e-05, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 3e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-4-vision-preview": { + "deprecation_date": "2024-12-06", + "input_cost_per_token": 1e-05, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 3e-05, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gpt-4.1": { + "cache_read_input_token_cost": 5e-07, + "cache_read_input_token_cost_priority": 8.75e-07, + "input_cost_per_token": 2e-06, + "input_cost_per_token_batches": 1e-06, + "input_cost_per_token_priority": 3.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 8e-06, + "output_cost_per_token_batches": 4e-06, + "output_cost_per_token_priority": 1.4e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "gpt-4.1-2025-04-14": { + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 2e-06, + "input_cost_per_token_batches": 1e-06, + "litellm_provider": "openai", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 8e-06, + "output_cost_per_token_batches": 4e-06, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "gpt-4.1-mini": { + "cache_read_input_token_cost": 1e-07, + "cache_read_input_token_cost_priority": 1.75e-07, + "input_cost_per_token": 4e-07, + "input_cost_per_token_batches": 2e-07, + "input_cost_per_token_priority": 7e-07, + "litellm_provider": "openai", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 1.6e-06, + "output_cost_per_token_batches": 8e-07, + "output_cost_per_token_priority": 2.8e-06, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "gpt-4.1-mini-2025-04-14": { + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 4e-07, + "input_cost_per_token_batches": 2e-07, + "litellm_provider": "openai", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 1.6e-06, + "output_cost_per_token_batches": 8e-07, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "gpt-4.1-nano": { + "cache_read_input_token_cost": 2.5e-08, + "cache_read_input_token_cost_priority": 5e-08, + "input_cost_per_token": 1e-07, + "input_cost_per_token_batches": 5e-08, + "input_cost_per_token_priority": 2e-07, + "litellm_provider": "openai", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 4e-07, + "output_cost_per_token_batches": 2e-07, + "output_cost_per_token_priority": 8e-07, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "gpt-4.1-nano-2025-04-14": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_token": 1e-07, + "input_cost_per_token_batches": 5e-08, + "litellm_provider": "openai", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 4e-07, + "output_cost_per_token_batches": 2e-07, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "gpt-4.5-preview": { + "cache_read_input_token_cost": 3.75e-05, + "input_cost_per_token": 7.5e-05, + "input_cost_per_token_batches": 3.75e-05, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 0.00015, + "output_cost_per_token_batches": 7.5e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gpt-4.5-preview-2025-02-27": { + "cache_read_input_token_cost": 3.75e-05, + "deprecation_date": "2025-07-14", + "input_cost_per_token": 7.5e-05, + "input_cost_per_token_batches": 3.75e-05, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 0.00015, + "output_cost_per_token_batches": 7.5e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gpt-4o": { + "cache_read_input_token_cost": 1.25e-06, + "cache_read_input_token_cost_priority": 2.125e-06, + "input_cost_per_token": 2.5e-06, + "input_cost_per_token_batches": 1.25e-06, + "input_cost_per_token_priority": 4.25e-06, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_batches": 5e-06, + "output_cost_per_token_priority": 1.7e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "gpt-4o-2024-05-13": { + "input_cost_per_token": 5e-06, + "input_cost_per_token_batches": 2.5e-06, + "input_cost_per_token_priority": 8.75e-06, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "output_cost_per_token_batches": 7.5e-06, + "output_cost_per_token_priority": 2.625e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gpt-4o-2024-08-06": { + "cache_read_input_token_cost": 1.25e-06, + "input_cost_per_token": 2.5e-06, + "input_cost_per_token_batches": 1.25e-06, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_batches": 5e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "gpt-4o-2024-11-20": { + "cache_read_input_token_cost": 1.25e-06, + "input_cost_per_token": 2.5e-06, + "input_cost_per_token_batches": 1.25e-06, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_batches": 5e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "gpt-4o-audio-preview": { + "input_cost_per_audio_token": 0.0001, + "input_cost_per_token": 2.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_audio_token": 0.0002, + "output_cost_per_token": 1e-05, + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-4o-audio-preview-2024-10-01": { + "input_cost_per_audio_token": 0.0001, + "input_cost_per_token": 2.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_audio_token": 0.0002, + "output_cost_per_token": 1e-05, + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-4o-audio-preview-2024-12-17": { + "input_cost_per_audio_token": 4e-05, + "input_cost_per_token": 2.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_audio_token": 8e-05, + "output_cost_per_token": 1e-05, + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-4o-audio-preview-2025-06-03": { + "input_cost_per_audio_token": 4e-05, + "input_cost_per_token": 2.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_audio_token": 8e-05, + "output_cost_per_token": 1e-05, + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-4o-mini": { + "cache_read_input_token_cost": 7.5e-08, + "cache_read_input_token_cost_priority": 1.25e-07, + "input_cost_per_token": 1.5e-07, + "input_cost_per_token_batches": 7.5e-08, + "input_cost_per_token_priority": 2.5e-07, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 6e-07, + "output_cost_per_token_batches": 3e-07, + "output_cost_per_token_priority": 1e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "gpt-4o-mini-2024-07-18": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_token": 1.5e-07, + "input_cost_per_token_batches": 7.5e-08, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 6e-07, + "output_cost_per_token_batches": 3e-07, + "search_context_cost_per_query": { + "search_context_size_high": 0.03, + "search_context_size_low": 0.025, + "search_context_size_medium": 0.0275 + }, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "gpt-4o-mini-audio-preview": { + "input_cost_per_audio_token": 1e-05, + "input_cost_per_token": 1.5e-07, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_audio_token": 2e-05, + "output_cost_per_token": 6e-07, + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-4o-mini-audio-preview-2024-12-17": { + "input_cost_per_audio_token": 1e-05, + "input_cost_per_token": 1.5e-07, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_audio_token": 2e-05, + "output_cost_per_token": 6e-07, + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-4o-mini-realtime-preview": { + "cache_creation_input_audio_token_cost": 3e-07, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_audio_token": 1e-05, + "input_cost_per_token": 6e-07, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_audio_token": 2e-05, + "output_cost_per_token": 2.4e-06, + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-4o-mini-realtime-preview-2024-12-17": { + "cache_creation_input_audio_token_cost": 3e-07, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_audio_token": 1e-05, + "input_cost_per_token": 6e-07, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_audio_token": 2e-05, + "output_cost_per_token": 2.4e-06, + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-4o-mini-search-preview": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_token": 1.5e-07, + "input_cost_per_token_batches": 7.5e-08, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 6e-07, + "output_cost_per_token_batches": 3e-07, + "search_context_cost_per_query": { + "search_context_size_high": 0.03, + "search_context_size_low": 0.025, + "search_context_size_medium": 0.0275 + }, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "gpt-4o-mini-search-preview-2025-03-11": { + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_token": 1.5e-07, + "input_cost_per_token_batches": 7.5e-08, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 6e-07, + "output_cost_per_token_batches": 3e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gpt-4o-mini-transcribe": { + "input_cost_per_audio_token": 3e-06, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "openai", + "max_input_tokens": 16000, + "max_output_tokens": 2000, + "mode": "audio_transcription", + "output_cost_per_token": 5e-06, + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "gpt-4o-mini-tts": { + "input_cost_per_token": 2.5e-06, + "litellm_provider": "openai", + "mode": "audio_speech", + "output_cost_per_audio_token": 1.2e-05, + "output_cost_per_second": 0.00025, + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/audio/speech" + ], + "supported_modalities": [ + "text", + "audio" + ], + "supported_output_modalities": [ + "audio" + ] + }, + "gpt-4o-realtime-preview": { + "cache_read_input_token_cost": 2.5e-06, + "input_cost_per_audio_token": 4e-05, + "input_cost_per_token": 5e-06, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_audio_token": 8e-05, + "output_cost_per_token": 2e-05, + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-4o-realtime-preview-2024-10-01": { + "cache_creation_input_audio_token_cost": 2e-05, + "cache_read_input_token_cost": 2.5e-06, + "input_cost_per_audio_token": 0.0001, + "input_cost_per_token": 5e-06, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_audio_token": 0.0002, + "output_cost_per_token": 2e-05, + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-4o-realtime-preview-2024-12-17": { + "cache_read_input_token_cost": 2.5e-06, + "input_cost_per_audio_token": 4e-05, + "input_cost_per_token": 5e-06, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_audio_token": 8e-05, + "output_cost_per_token": 2e-05, + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-4o-realtime-preview-2025-06-03": { + "cache_read_input_token_cost": 2.5e-06, + "input_cost_per_audio_token": 4e-05, + "input_cost_per_token": 5e-06, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_audio_token": 8e-05, + "output_cost_per_token": 2e-05, + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-4o-search-preview": { + "cache_read_input_token_cost": 1.25e-06, + "input_cost_per_token": 2.5e-06, + "input_cost_per_token_batches": 1.25e-06, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_batches": 5e-06, + "search_context_cost_per_query": { + "search_context_size_high": 0.05, + "search_context_size_low": 0.03, + "search_context_size_medium": 0.035 + }, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "gpt-4o-search-preview-2025-03-11": { + "cache_read_input_token_cost": 1.25e-06, + "input_cost_per_token": 2.5e-06, + "input_cost_per_token_batches": 1.25e-06, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_batches": 5e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gpt-4o-transcribe": { + "input_cost_per_audio_token": 6e-06, + "input_cost_per_token": 2.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 16000, + "max_output_tokens": 2000, + "mode": "audio_transcription", + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "gpt-image-1.5": { + "cache_read_input_image_token_cost": 2e-06, + "cache_read_input_token_cost": 1.25e-06, + "input_cost_per_token": 5e-06, + "litellm_provider": "openai", + "mode": "image_generation", + "output_cost_per_token": 1e-05, + "input_cost_per_image_token": 8e-06, + "output_cost_per_image_token": 3.2e-05, + "supported_endpoints": [ + "/v1/images/generations" + ], + "supports_vision": true + }, + "gpt-image-1.5-2025-12-16": { + "cache_read_input_image_token_cost": 2e-06, + "cache_read_input_token_cost": 1.25e-06, + "input_cost_per_token": 5e-06, + "litellm_provider": "openai", + "mode": "image_generation", + "output_cost_per_token": 1e-05, + "input_cost_per_image_token": 8e-06, + "output_cost_per_image_token": 3.2e-05, + "supported_endpoints": [ + "/v1/images/generations" + ], + "supports_vision": true + }, + "gpt-5": { + "cache_read_input_token_cost": 1.25e-07, + "cache_read_input_token_cost_flex": 6.25e-08, + "cache_read_input_token_cost_priority": 2.5e-07, + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_flex": 6.25e-07, + "input_cost_per_token_priority": 2.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_flex": 5e-06, + "output_cost_per_token_priority": 2e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "gpt-5.1": { + "cache_read_input_token_cost": 1.25e-07, + "cache_read_input_token_cost_priority": 2.5e-07, + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_priority": 2.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_priority": 2e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "gpt-5.1-2025-11-13": { + "cache_read_input_token_cost": 1.25e-07, + "cache_read_input_token_cost_priority": 2.5e-07, + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_priority": 2.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_priority": 2e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "gpt-5.1-chat-latest": { + "cache_read_input_token_cost": 1.25e-07, + "cache_read_input_token_cost_priority": 2.5e-07, + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_priority": 2.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_priority": 2e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_function_calling": false, + "supports_native_streaming": true, + "supports_parallel_function_calling": false, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": false, + "supports_vision": true + }, + "gpt-5.2": { + "cache_read_input_token_cost": 1.75e-07, + "cache_read_input_token_cost_priority": 3.5e-07, + "input_cost_per_token": 1.75e-06, + "input_cost_per_token_priority": 3.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.4e-05, + "output_cost_per_token_priority": 2.8e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "gpt-5.2-2025-12-11": { + "cache_read_input_token_cost": 1.75e-07, + "cache_read_input_token_cost_priority": 3.5e-07, + "input_cost_per_token": 1.75e-06, + "input_cost_per_token_priority": 3.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.4e-05, + "output_cost_per_token_priority": 2.8e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "gpt-5.2-chat-latest": { + "cache_read_input_token_cost": 1.75e-07, + "cache_read_input_token_cost_priority": 3.5e-07, + "input_cost_per_token": 1.75e-06, + "input_cost_per_token_priority": 3.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1.4e-05, + "output_cost_per_token_priority": 2.8e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gpt-5.2-pro": { + "input_cost_per_token": 2.1e-05, + "litellm_provider": "openai", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "output_cost_per_token": 1.68e-04, + "supported_endpoints": [ + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "gpt-5.2-pro-2025-12-11": { + "input_cost_per_token": 2.1e-05, + "litellm_provider": "openai", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "output_cost_per_token": 1.68e-04, + "supported_endpoints": [ + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "gpt-5-pro": { + "input_cost_per_token": 1.5e-05, + "input_cost_per_token_batches": 7.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 400000, + "max_output_tokens": 272000, + "max_tokens": 272000, + "mode": "responses", + "output_cost_per_token": 1.2e-04, + "output_cost_per_token_batches": 6e-05, + "supported_endpoints": [ + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": false, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "gpt-5-pro-2025-10-06": { + "input_cost_per_token": 1.5e-05, + "input_cost_per_token_batches": 7.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 400000, + "max_output_tokens": 272000, + "max_tokens": 272000, + "mode": "responses", + "output_cost_per_token": 1.2e-04, + "output_cost_per_token_batches": 6e-05, + "supported_endpoints": [ + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": false, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "gpt-5-2025-08-07": { + "cache_read_input_token_cost": 1.25e-07, + "cache_read_input_token_cost_flex": 6.25e-08, + "cache_read_input_token_cost_priority": 2.5e-07, + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_flex": 6.25e-07, + "input_cost_per_token_priority": 2.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_flex": 5e-06, + "output_cost_per_token_priority": 2e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "gpt-5-chat": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "openai", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": false, + "supports_native_streaming": true, + "supports_parallel_function_calling": false, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": false, + "supports_vision": true + }, + "gpt-5-chat-latest": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": false, + "supports_native_streaming": true, + "supports_parallel_function_calling": false, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": false, + "supports_vision": true + }, + "gpt-5-codex": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "openai", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "gpt-5.1-codex": { + "cache_read_input_token_cost": 1.25e-07, + "cache_read_input_token_cost_priority": 2.5e-07, + "input_cost_per_token": 1.25e-06, + "input_cost_per_token_priority": 2.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "output_cost_per_token": 1e-05, + "output_cost_per_token_priority": 2e-05, + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "gpt-5.1-codex-max": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "openai", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "output_cost_per_token": 1e-05, + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "gpt-5.1-codex-mini": { + "cache_read_input_token_cost": 2.5e-08, + "cache_read_input_token_cost_priority": 4.5e-08, + "input_cost_per_token": 2.5e-07, + "input_cost_per_token_priority": 4.5e-07, + "litellm_provider": "openai", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "responses", + "output_cost_per_token": 2e-06, + "output_cost_per_token_priority": 3.6e-06, + "supported_endpoints": [ + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "gpt-5-mini": { + "cache_read_input_token_cost": 2.5e-08, + "cache_read_input_token_cost_flex": 1.25e-08, + "cache_read_input_token_cost_priority": 4.5e-08, + "input_cost_per_token": 2.5e-07, + "input_cost_per_token_flex": 1.25e-07, + "input_cost_per_token_priority": 4.5e-07, + "litellm_provider": "openai", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 2e-06, + "output_cost_per_token_flex": 1e-06, + "output_cost_per_token_priority": 3.6e-06, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "gpt-5-mini-2025-08-07": { + "cache_read_input_token_cost": 2.5e-08, + "cache_read_input_token_cost_flex": 1.25e-08, + "cache_read_input_token_cost_priority": 4.5e-08, + "input_cost_per_token": 2.5e-07, + "input_cost_per_token_flex": 1.25e-07, + "input_cost_per_token_priority": 4.5e-07, + "litellm_provider": "openai", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 2e-06, + "output_cost_per_token_flex": 1e-06, + "output_cost_per_token_priority": 3.6e-06, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "gpt-5-nano": { + "cache_read_input_token_cost": 5e-09, + "cache_read_input_token_cost_flex": 2.5e-09, + "input_cost_per_token": 5e-08, + "input_cost_per_token_flex": 2.5e-08, + "input_cost_per_token_priority": 2.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 4e-07, + "output_cost_per_token_flex": 2e-07, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gpt-5-nano-2025-08-07": { + "cache_read_input_token_cost": 5e-09, + "cache_read_input_token_cost_flex": 2.5e-09, + "input_cost_per_token": 5e-08, + "input_cost_per_token_flex": 2.5e-08, + "litellm_provider": "openai", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 4e-07, + "output_cost_per_token_flex": 2e-07, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "gpt-image-1": { + "input_cost_per_image": 0.042, + "input_cost_per_pixel": 4.0054321e-08, + "input_cost_per_token": 0.000005, + "input_cost_per_image_token": 0.00001, + "litellm_provider": "openai", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "output_cost_per_token": 0.00004, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "gpt-image-1-mini": { + "cache_read_input_image_token_cost": 2.5e-07, + "cache_read_input_token_cost": 2e-07, + "input_cost_per_image_token": 2.5e-06, + "input_cost_per_token": 2e-06, + "litellm_provider": "openai", + "mode": "image_generation", + "output_cost_per_image_token": 8e-06, + "supported_endpoints": [ + "/v1/images/generations", + "/v1/images/edits" + ] + }, + "gpt-realtime": { + "cache_creation_input_audio_token_cost": 4e-07, + "cache_read_input_token_cost": 4e-07, + "input_cost_per_audio_token": 3.2e-05, + "input_cost_per_image": 5e-06, + "input_cost_per_token": 4e-06, + "litellm_provider": "openai", + "max_input_tokens": 32000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_audio_token": 6.4e-05, + "output_cost_per_token": 1.6e-05, + "supported_endpoints": [ + "/v1/realtime" + ], + "supported_modalities": [ + "text", + "image", + "audio" + ], + "supported_output_modalities": [ + "text", + "audio" + ], + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-realtime-mini": { + "cache_creation_input_audio_token_cost": 3e-07, + "cache_read_input_audio_token_cost": 3e-07, + "input_cost_per_audio_token": 1e-05, + "input_cost_per_token": 6e-07, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_audio_token": 2e-05, + "output_cost_per_token": 2.4e-06, + "supported_endpoints": [ + "/v1/realtime" + ], + "supported_modalities": [ + "text", + "image", + "audio" + ], + "supported_output_modalities": [ + "text", + "audio" + ], + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gpt-realtime-2025-08-28": { + "cache_creation_input_audio_token_cost": 4e-07, + "cache_read_input_token_cost": 4e-07, + "input_cost_per_audio_token": 3.2e-05, + "input_cost_per_image": 5e-06, + "input_cost_per_token": 4e-06, + "litellm_provider": "openai", + "max_input_tokens": 32000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_audio_token": 6.4e-05, + "output_cost_per_token": 1.6e-05, + "supported_endpoints": [ + "/v1/realtime" + ], + "supported_modalities": [ + "text", + "image", + "audio" + ], + "supported_output_modalities": [ + "text", + "audio" + ], + "supports_audio_input": true, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "gradient_ai/alibaba-qwen3-32b": { + "litellm_provider": "gradient_ai", + "max_tokens": 2048, + "mode": "chat", + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supported_modalities": [ + "text" + ], + "supports_tool_choice": false + }, + "gradient_ai/anthropic-claude-3-opus": { + "input_cost_per_token": 1.5e-05, + "litellm_provider": "gradient_ai", + "max_tokens": 1024, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supported_modalities": [ + "text" + ], + "supports_tool_choice": false + }, + "gradient_ai/anthropic-claude-3.5-haiku": { + "input_cost_per_token": 8e-07, + "litellm_provider": "gradient_ai", + "max_tokens": 1024, + "mode": "chat", + "output_cost_per_token": 4e-06, + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supported_modalities": [ + "text" + ], + "supports_tool_choice": false + }, + "gradient_ai/anthropic-claude-3.5-sonnet": { + "input_cost_per_token": 3e-06, + "litellm_provider": "gradient_ai", + "max_tokens": 1024, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supported_modalities": [ + "text" + ], + "supports_tool_choice": false + }, + "gradient_ai/anthropic-claude-3.7-sonnet": { + "input_cost_per_token": 3e-06, + "litellm_provider": "gradient_ai", + "max_tokens": 1024, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supported_modalities": [ + "text" + ], + "supports_tool_choice": false + }, + "gradient_ai/deepseek-r1-distill-llama-70b": { + "input_cost_per_token": 9.9e-07, + "litellm_provider": "gradient_ai", + "max_tokens": 8000, + "mode": "chat", + "output_cost_per_token": 9.9e-07, + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supported_modalities": [ + "text" + ], + "supports_tool_choice": false + }, + "gradient_ai/llama3-8b-instruct": { + "input_cost_per_token": 2e-07, + "litellm_provider": "gradient_ai", + "max_tokens": 512, + "mode": "chat", + "output_cost_per_token": 2e-07, + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supported_modalities": [ + "text" + ], + "supports_tool_choice": false + }, + "gradient_ai/llama3.3-70b-instruct": { + "input_cost_per_token": 6.5e-07, + "litellm_provider": "gradient_ai", + "max_tokens": 2048, + "mode": "chat", + "output_cost_per_token": 6.5e-07, + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supported_modalities": [ + "text" + ], + "supports_tool_choice": false + }, + "gradient_ai/mistral-nemo-instruct-2407": { + "input_cost_per_token": 3e-07, + "litellm_provider": "gradient_ai", + "max_tokens": 512, + "mode": "chat", + "output_cost_per_token": 3e-07, + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supported_modalities": [ + "text" + ], + "supports_tool_choice": false + }, + "gradient_ai/openai-gpt-4o": { + "litellm_provider": "gradient_ai", + "max_tokens": 16384, + "mode": "chat", + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supported_modalities": [ + "text" + ], + "supports_tool_choice": false + }, + "gradient_ai/openai-gpt-4o-mini": { + "litellm_provider": "gradient_ai", + "max_tokens": 16384, + "mode": "chat", + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supported_modalities": [ + "text" + ], + "supports_tool_choice": false + }, + "gradient_ai/openai-o3": { + "input_cost_per_token": 2e-06, + "litellm_provider": "gradient_ai", + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 8e-06, + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supported_modalities": [ + "text" + ], + "supports_tool_choice": false + }, + "gradient_ai/openai-o3-mini": { + "input_cost_per_token": 1.1e-06, + "litellm_provider": "gradient_ai", + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 4.4e-06, + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supported_modalities": [ + "text" + ], + "supports_tool_choice": false + }, + "lemonade/Qwen3-Coder-30B-A3B-Instruct-GGUF": { + "input_cost_per_token": 0, + "litellm_provider": "lemonade", + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 0, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "lemonade/gpt-oss-20b-mxfp4-GGUF": { + "input_cost_per_token": 0, + "litellm_provider": "lemonade", + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 0, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "lemonade/gpt-oss-120b-mxfp-GGUF": { + "input_cost_per_token": 0, + "litellm_provider": "lemonade", + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 0, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "lemonade/Gemma-3-4b-it-GGUF": { + "input_cost_per_token": 0, + "litellm_provider": "lemonade", + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 0, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "lemonade/Qwen3-4B-Instruct-2507-GGUF": { + "input_cost_per_token": 0, + "litellm_provider": "lemonade", + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 0, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "amazon-nova/nova-micro-v1": { + "input_cost_per_token": 3.5e-08, + "litellm_provider": "amazon_nova", + "max_input_tokens": 128000, + "max_output_tokens": 10000, + "max_tokens": 10000, + "mode": "chat", + "output_cost_per_token": 1.4e-07, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true + }, + "amazon-nova/nova-lite-v1": { + "input_cost_per_token": 6e-08, + "litellm_provider": "amazon_nova", + "max_input_tokens": 300000, + "max_output_tokens": 10000, + "max_tokens": 10000, + "mode": "chat", + "output_cost_per_token": 2.4e-07, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_vision": true + }, + "amazon-nova/nova-premier-v1": { + "input_cost_per_token": 2.5e-06, + "litellm_provider": "amazon_nova", + "max_input_tokens": 1000000, + "max_output_tokens": 10000, + "max_tokens": 10000, + "mode": "chat", + "output_cost_per_token": 1.25e-05, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": false, + "supports_response_schema": true, + "supports_vision": true + }, + "amazon-nova/nova-pro-v1": { + "input_cost_per_token": 8e-07, + "litellm_provider": "amazon_nova", + "max_input_tokens": 300000, + "max_output_tokens": 10000, + "max_tokens": 10000, + "mode": "chat", + "output_cost_per_token": 3.2e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_vision": true + }, + "groq/deepseek-r1-distill-llama-70b": { + "input_cost_per_token": 7.5e-07, + "litellm_provider": "groq", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 9.9e-07, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": false, + "supports_tool_choice": true + }, + "groq/distil-whisper-large-v3-en": { + "input_cost_per_second": 5.56e-06, + "litellm_provider": "groq", + "mode": "audio_transcription", + "output_cost_per_second": 0.0 + }, + "groq/gemma-7b-it": { + "deprecation_date": "2024-12-18", + "input_cost_per_token": 7e-08, + "litellm_provider": "groq", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 7e-08, + "supports_function_calling": true, + "supports_response_schema": false, + "supports_tool_choice": true + }, + "groq/gemma2-9b-it": { + "input_cost_per_token": 2e-07, + "litellm_provider": "groq", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2e-07, + "supports_function_calling": false, + "supports_response_schema": false, + "supports_tool_choice": false + }, + "groq/llama-3.1-405b-reasoning": { + "input_cost_per_token": 5.9e-07, + "litellm_provider": "groq", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 7.9e-07, + "supports_function_calling": true, + "supports_response_schema": false, + "supports_tool_choice": true + }, + "groq/llama-3.1-70b-versatile": { + "deprecation_date": "2025-01-24", + "input_cost_per_token": 5.9e-07, + "litellm_provider": "groq", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 7.9e-07, + "supports_function_calling": true, + "supports_response_schema": false, + "supports_tool_choice": true + }, + "groq/llama-3.1-8b-instant": { + "input_cost_per_token": 5e-08, + "litellm_provider": "groq", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 8e-08, + "supports_function_calling": true, + "supports_response_schema": false, + "supports_tool_choice": true + }, + "groq/llama-3.2-11b-text-preview": { + "deprecation_date": "2024-10-28", + "input_cost_per_token": 1.8e-07, + "litellm_provider": "groq", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.8e-07, + "supports_function_calling": true, + "supports_response_schema": false, + "supports_tool_choice": true + }, + "groq/llama-3.2-11b-vision-preview": { + "deprecation_date": "2025-04-14", + "input_cost_per_token": 1.8e-07, + "litellm_provider": "groq", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.8e-07, + "supports_function_calling": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "groq/llama-3.2-1b-preview": { + "deprecation_date": "2025-04-14", + "input_cost_per_token": 4e-08, + "litellm_provider": "groq", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 4e-08, + "supports_function_calling": true, + "supports_response_schema": false, + "supports_tool_choice": true + }, + "groq/llama-3.2-3b-preview": { + "deprecation_date": "2025-04-14", + "input_cost_per_token": 6e-08, + "litellm_provider": "groq", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 6e-08, + "supports_function_calling": true, + "supports_response_schema": false, + "supports_tool_choice": true + }, + "groq/llama-3.2-90b-text-preview": { + "deprecation_date": "2024-11-25", + "input_cost_per_token": 9e-07, + "litellm_provider": "groq", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 9e-07, + "supports_function_calling": true, + "supports_response_schema": false, + "supports_tool_choice": true + }, + "groq/llama-3.2-90b-vision-preview": { + "deprecation_date": "2025-04-14", + "input_cost_per_token": 9e-07, + "litellm_provider": "groq", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 9e-07, + "supports_function_calling": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "groq/llama-3.3-70b-specdec": { + "deprecation_date": "2025-04-14", + "input_cost_per_token": 5.9e-07, + "litellm_provider": "groq", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 9.9e-07, + "supports_tool_choice": true + }, + "groq/llama-3.3-70b-versatile": { + "input_cost_per_token": 5.9e-07, + "litellm_provider": "groq", + "max_input_tokens": 128000, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 7.9e-07, + "supports_function_calling": true, + "supports_response_schema": false, + "supports_tool_choice": true + }, + "groq/llama-guard-3-8b": { + "input_cost_per_token": 2e-07, + "litellm_provider": "groq", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2e-07 + }, + "groq/llama2-70b-4096": { + "input_cost_per_token": 7e-07, + "litellm_provider": "groq", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 8e-07, + "supports_function_calling": true, + "supports_response_schema": false, + "supports_tool_choice": true + }, + "groq/llama3-groq-70b-8192-tool-use-preview": { + "deprecation_date": "2025-01-06", + "input_cost_per_token": 8.9e-07, + "litellm_provider": "groq", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 8.9e-07, + "supports_function_calling": true, + "supports_response_schema": false, + "supports_tool_choice": true + }, + "groq/llama3-groq-8b-8192-tool-use-preview": { + "deprecation_date": "2025-01-06", + "input_cost_per_token": 1.9e-07, + "litellm_provider": "groq", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.9e-07, + "supports_function_calling": true, + "supports_response_schema": false, + "supports_tool_choice": true + }, + "groq/meta-llama/llama-4-maverick-17b-128e-instruct": { + "input_cost_per_token": 2e-07, + "litellm_provider": "groq", + "max_input_tokens": 131072, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 6e-07, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "groq/meta-llama/llama-4-scout-17b-16e-instruct": { + "input_cost_per_token": 1.1e-07, + "litellm_provider": "groq", + "max_input_tokens": 131072, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 3.4e-07, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "groq/mistral-saba-24b": { + "input_cost_per_token": 7.9e-07, + "litellm_provider": "groq", + "max_input_tokens": 32000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 7.9e-07 + }, + "groq/mixtral-8x7b-32768": { + "deprecation_date": "2025-03-20", + "input_cost_per_token": 2.4e-07, + "litellm_provider": "groq", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 2.4e-07, + "supports_function_calling": true, + "supports_response_schema": false, + "supports_tool_choice": true + }, + "groq/moonshotai/kimi-k2-instruct": { + "input_cost_per_token": 1e-06, + "litellm_provider": "groq", + "max_input_tokens": 131072, + "max_output_tokens": 16384, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 3e-06, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "groq/moonshotai/kimi-k2-instruct-0905": { + "input_cost_per_token": 1e-06, + "output_cost_per_token": 3e-06, + "cache_read_input_token_cost": 0.5e-06, + "litellm_provider": "groq", + "max_input_tokens": 262144, + "max_output_tokens": 16384, + "max_tokens": 278528, + "mode": "chat", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "groq/openai/gpt-oss-120b": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "groq", + "max_input_tokens": 131072, + "max_output_tokens": 32766, + "max_tokens": 32766, + "mode": "chat", + "output_cost_per_token": 7.5e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "groq/openai/gpt-oss-20b": { + "input_cost_per_token": 1e-07, + "litellm_provider": "groq", + "max_input_tokens": 131072, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 5e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "groq/playai-tts": { + "input_cost_per_character": 5e-05, + "litellm_provider": "groq", + "max_input_tokens": 10000, + "max_output_tokens": 10000, + "max_tokens": 10000, + "mode": "audio_speech" + }, + "groq/qwen/qwen3-32b": { + "input_cost_per_token": 2.9e-07, + "litellm_provider": "groq", + "max_input_tokens": 131000, + "max_output_tokens": 131000, + "max_tokens": 131000, + "mode": "chat", + "output_cost_per_token": 5.9e-07, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": false, + "supports_tool_choice": true + }, + "groq/whisper-large-v3": { + "input_cost_per_second": 3.083e-05, + "litellm_provider": "groq", + "mode": "audio_transcription", + "output_cost_per_second": 0.0 + }, + "groq/whisper-large-v3-turbo": { + "input_cost_per_second": 1.111e-05, + "litellm_provider": "groq", + "mode": "audio_transcription", + "output_cost_per_second": 0.0 + }, + "hd/1024-x-1024/dall-e-3": { + "input_cost_per_pixel": 7.629e-08, + "litellm_provider": "openai", + "mode": "image_generation", + "output_cost_per_pixel": 0.0 + }, + "hd/1024-x-1792/dall-e-3": { + "input_cost_per_pixel": 6.539e-08, + "litellm_provider": "openai", + "mode": "image_generation", + "output_cost_per_pixel": 0.0 + }, + "hd/1792-x-1024/dall-e-3": { + "input_cost_per_pixel": 6.539e-08, + "litellm_provider": "openai", + "mode": "image_generation", + "output_cost_per_pixel": 0.0 + }, + "heroku/claude-3-5-haiku": { + "litellm_provider": "heroku", + "max_tokens": 4096, + "mode": "chat", + "supports_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "heroku/claude-3-5-sonnet-latest": { + "litellm_provider": "heroku", + "max_tokens": 8192, + "mode": "chat", + "supports_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "heroku/claude-3-7-sonnet": { + "litellm_provider": "heroku", + "max_tokens": 8192, + "mode": "chat", + "supports_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "heroku/claude-4-sonnet": { + "litellm_provider": "heroku", + "max_tokens": 8192, + "mode": "chat", + "supports_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "high/1024-x-1024/gpt-image-1": { + "input_cost_per_image": 0.167, + "input_cost_per_pixel": 1.59263611e-07, + "litellm_provider": "openai", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "high/1024-x-1536/gpt-image-1": { + "input_cost_per_image": 0.25, + "input_cost_per_pixel": 1.58945719e-07, + "litellm_provider": "openai", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "high/1536-x-1024/gpt-image-1": { + "input_cost_per_image": 0.25, + "input_cost_per_pixel": 1.58945719e-07, + "litellm_provider": "openai", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "hyperbolic/NousResearch/Hermes-3-Llama-3.1-70B": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "hyperbolic", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 3e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "hyperbolic/Qwen/QwQ-32B": { + "input_cost_per_token": 2e-07, + "litellm_provider": "hyperbolic", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "hyperbolic/Qwen/Qwen2.5-72B-Instruct": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "hyperbolic", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 3e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "hyperbolic/Qwen/Qwen2.5-Coder-32B-Instruct": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "hyperbolic", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 3e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "hyperbolic/Qwen/Qwen3-235B-A22B": { + "input_cost_per_token": 2e-06, + "litellm_provider": "hyperbolic", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "hyperbolic/deepseek-ai/DeepSeek-R1": { + "input_cost_per_token": 4e-07, + "litellm_provider": "hyperbolic", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "hyperbolic/deepseek-ai/DeepSeek-R1-0528": { + "input_cost_per_token": 2.5e-07, + "litellm_provider": "hyperbolic", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2.5e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "hyperbolic/deepseek-ai/DeepSeek-V3": { + "input_cost_per_token": 2e-07, + "litellm_provider": "hyperbolic", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 2e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "hyperbolic/deepseek-ai/DeepSeek-V3-0324": { + "input_cost_per_token": 4e-07, + "litellm_provider": "hyperbolic", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "hyperbolic/meta-llama/Llama-3.2-3B-Instruct": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "hyperbolic", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 3e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "hyperbolic/meta-llama/Llama-3.3-70B-Instruct": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "hyperbolic", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 3e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "hyperbolic/meta-llama/Meta-Llama-3-70B-Instruct": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "hyperbolic", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 3e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "hyperbolic/meta-llama/Meta-Llama-3.1-405B-Instruct": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "hyperbolic", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 3e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "hyperbolic/meta-llama/Meta-Llama-3.1-70B-Instruct": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "hyperbolic", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 3e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "hyperbolic/meta-llama/Meta-Llama-3.1-8B-Instruct": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "hyperbolic", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 3e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "hyperbolic/moonshotai/Kimi-K2-Instruct": { + "input_cost_per_token": 2e-06, + "litellm_provider": "hyperbolic", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "j2-light": { + "input_cost_per_token": 3e-06, + "litellm_provider": "ai21", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "completion", + "output_cost_per_token": 3e-06 + }, + "j2-mid": { + "input_cost_per_token": 1e-05, + "litellm_provider": "ai21", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "completion", + "output_cost_per_token": 1e-05 + }, + "j2-ultra": { + "input_cost_per_token": 1.5e-05, + "litellm_provider": "ai21", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "completion", + "output_cost_per_token": 1.5e-05 + }, + "jamba-1.5": { + "input_cost_per_token": 2e-07, + "litellm_provider": "ai21", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supports_tool_choice": true + }, + "jamba-1.5-large": { + "input_cost_per_token": 2e-06, + "litellm_provider": "ai21", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 8e-06, + "supports_tool_choice": true + }, + "jamba-1.5-large@001": { + "input_cost_per_token": 2e-06, + "litellm_provider": "ai21", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 8e-06, + "supports_tool_choice": true + }, + "jamba-1.5-mini": { + "input_cost_per_token": 2e-07, + "litellm_provider": "ai21", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supports_tool_choice": true + }, + "jamba-1.5-mini@001": { + "input_cost_per_token": 2e-07, + "litellm_provider": "ai21", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supports_tool_choice": true + }, + "jamba-large-1.6": { + "input_cost_per_token": 2e-06, + "litellm_provider": "ai21", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 8e-06, + "supports_tool_choice": true + }, + "jamba-large-1.7": { + "input_cost_per_token": 2e-06, + "litellm_provider": "ai21", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 8e-06, + "supports_tool_choice": true + }, + "jamba-mini-1.6": { + "input_cost_per_token": 2e-07, + "litellm_provider": "ai21", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supports_tool_choice": true + }, + "jamba-mini-1.7": { + "input_cost_per_token": 2e-07, + "litellm_provider": "ai21", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supports_tool_choice": true + }, + "jina-reranker-v2-base-multilingual": { + "input_cost_per_token": 1.8e-08, + "litellm_provider": "jina_ai", + "max_document_chunks_per_query": 2048, + "max_input_tokens": 1024, + "max_output_tokens": 1024, + "max_tokens": 1024, + "mode": "rerank", + "output_cost_per_token": 1.8e-08 + }, + "jp.anthropic.claude-sonnet-4-5-20250929-v1:0": { + "cache_creation_input_token_cost": 4.125e-06, + "cache_read_input_token_cost": 3.3e-07, + "input_cost_per_token": 3.3e-06, + "input_cost_per_token_above_200k_tokens": 6.6e-06, + "output_cost_per_token_above_200k_tokens": 2.475e-05, + "cache_creation_input_token_cost_above_200k_tokens": 8.25e-06, + "cache_read_input_token_cost_above_200k_tokens": 6.6e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.65e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 346 + }, + "jp.anthropic.claude-haiku-4-5-20251001-v1:0": { + "cache_creation_input_token_cost": 1.375e-06, + "cache_read_input_token_cost": 1.1e-07, + "input_cost_per_token": 1.1e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 5.5e-06, + "source": "https://aws.amazon.com/about-aws/whats-new/2025/10/claude-4-5-haiku-anthropic-amazon-bedrock", + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 346 + }, + "lambda_ai/deepseek-llama3.3-70b": { + "input_cost_per_token": 2e-07, + "litellm_provider": "lambda_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 6e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_reasoning": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "lambda_ai/deepseek-r1-0528": { + "input_cost_per_token": 2e-07, + "litellm_provider": "lambda_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 6e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_reasoning": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "lambda_ai/deepseek-r1-671b": { + "input_cost_per_token": 8e-07, + "litellm_provider": "lambda_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 8e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_reasoning": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "lambda_ai/deepseek-v3-0324": { + "input_cost_per_token": 2e-07, + "litellm_provider": "lambda_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 6e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "lambda_ai/hermes3-405b": { + "input_cost_per_token": 8e-07, + "litellm_provider": "lambda_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 8e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "lambda_ai/hermes3-70b": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "lambda_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 3e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "lambda_ai/hermes3-8b": { + "input_cost_per_token": 2.5e-08, + "litellm_provider": "lambda_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 4e-08, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "lambda_ai/lfm-40b": { + "input_cost_per_token": 1e-07, + "litellm_provider": "lambda_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "lambda_ai/lfm-7b": { + "input_cost_per_token": 2.5e-08, + "litellm_provider": "lambda_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 4e-08, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "lambda_ai/llama-4-maverick-17b-128e-instruct-fp8": { + "input_cost_per_token": 5e-08, + "litellm_provider": "lambda_ai", + "max_input_tokens": 131072, + "max_output_tokens": 8192, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "lambda_ai/llama-4-scout-17b-16e-instruct": { + "input_cost_per_token": 5e-08, + "litellm_provider": "lambda_ai", + "max_input_tokens": 16384, + "max_output_tokens": 8192, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "lambda_ai/llama3.1-405b-instruct-fp8": { + "input_cost_per_token": 8e-07, + "litellm_provider": "lambda_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 8e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "lambda_ai/llama3.1-70b-instruct-fp8": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "lambda_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 3e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "lambda_ai/llama3.1-8b-instruct": { + "input_cost_per_token": 2.5e-08, + "litellm_provider": "lambda_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 4e-08, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "lambda_ai/llama3.1-nemotron-70b-instruct-fp8": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "lambda_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 3e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "lambda_ai/llama3.2-11b-vision-instruct": { + "input_cost_per_token": 1.5e-08, + "litellm_provider": "lambda_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2.5e-08, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "lambda_ai/llama3.2-3b-instruct": { + "input_cost_per_token": 1.5e-08, + "litellm_provider": "lambda_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2.5e-08, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "lambda_ai/llama3.3-70b-instruct-fp8": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "lambda_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 3e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "lambda_ai/qwen25-coder-32b-instruct": { + "input_cost_per_token": 5e-08, + "litellm_provider": "lambda_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "lambda_ai/qwen3-32b-fp8": { + "input_cost_per_token": 5e-08, + "litellm_provider": "lambda_ai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_reasoning": true, + "supports_system_messages": true, + "supports_tool_choice": true + }, + "low/1024-x-1024/gpt-image-1": { + "input_cost_per_image": 0.011, + "input_cost_per_pixel": 1.0490417e-08, + "litellm_provider": "openai", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "low/1024-x-1536/gpt-image-1": { + "input_cost_per_image": 0.016, + "input_cost_per_pixel": 1.0172526e-08, + "litellm_provider": "openai", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "low/1536-x-1024/gpt-image-1": { + "input_cost_per_image": 0.016, + "input_cost_per_pixel": 1.0172526e-08, + "litellm_provider": "openai", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "luminous-base": { + "input_cost_per_token": 3e-05, + "litellm_provider": "aleph_alpha", + "max_tokens": 2048, + "mode": "completion", + "output_cost_per_token": 3.3e-05 + }, + "luminous-base-control": { + "input_cost_per_token": 3.75e-05, + "litellm_provider": "aleph_alpha", + "max_tokens": 2048, + "mode": "chat", + "output_cost_per_token": 4.125e-05 + }, + "luminous-extended": { + "input_cost_per_token": 4.5e-05, + "litellm_provider": "aleph_alpha", + "max_tokens": 2048, + "mode": "completion", + "output_cost_per_token": 4.95e-05 + }, + "luminous-extended-control": { + "input_cost_per_token": 5.625e-05, + "litellm_provider": "aleph_alpha", + "max_tokens": 2048, + "mode": "chat", + "output_cost_per_token": 6.1875e-05 + }, + "luminous-supreme": { + "input_cost_per_token": 0.000175, + "litellm_provider": "aleph_alpha", + "max_tokens": 2048, + "mode": "completion", + "output_cost_per_token": 0.0001925 + }, + "luminous-supreme-control": { + "input_cost_per_token": 0.00021875, + "litellm_provider": "aleph_alpha", + "max_tokens": 2048, + "mode": "chat", + "output_cost_per_token": 0.000240625 + }, + "max-x-max/50-steps/stability.stable-diffusion-xl-v0": { + "litellm_provider": "bedrock", + "max_input_tokens": 77, + "max_tokens": 77, + "mode": "image_generation", + "output_cost_per_image": 0.036 + }, + "max-x-max/max-steps/stability.stable-diffusion-xl-v0": { + "litellm_provider": "bedrock", + "max_input_tokens": 77, + "max_tokens": 77, + "mode": "image_generation", + "output_cost_per_image": 0.072 + }, + "medium/1024-x-1024/gpt-image-1": { + "input_cost_per_image": 0.042, + "input_cost_per_pixel": 4.0054321e-08, + "litellm_provider": "openai", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "medium/1024-x-1536/gpt-image-1": { + "input_cost_per_image": 0.063, + "input_cost_per_pixel": 4.0054321e-08, + "litellm_provider": "openai", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "medium/1536-x-1024/gpt-image-1": { + "input_cost_per_image": 0.063, + "input_cost_per_pixel": 4.0054321e-08, + "litellm_provider": "openai", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "low/1024-x-1024/gpt-image-1-mini": { + "input_cost_per_image": 0.005, + "litellm_provider": "openai", + "mode": "image_generation", + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "low/1024-x-1536/gpt-image-1-mini": { + "input_cost_per_image": 0.006, + "litellm_provider": "openai", + "mode": "image_generation", + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "low/1536-x-1024/gpt-image-1-mini": { + "input_cost_per_image": 0.006, + "litellm_provider": "openai", + "mode": "image_generation", + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "medium/1024-x-1024/gpt-image-1-mini": { + "input_cost_per_image": 0.011, + "litellm_provider": "openai", + "mode": "image_generation", + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "medium/1024-x-1536/gpt-image-1-mini": { + "input_cost_per_image": 0.015, + "litellm_provider": "openai", + "mode": "image_generation", + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "medium/1536-x-1024/gpt-image-1-mini": { + "input_cost_per_image": 0.015, + "litellm_provider": "openai", + "mode": "image_generation", + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "medlm-large": { + "input_cost_per_character": 5e-06, + "litellm_provider": "vertex_ai-language-models", + "max_input_tokens": 8192, + "max_output_tokens": 1024, + "max_tokens": 1024, + "mode": "chat", + "output_cost_per_character": 1.5e-05, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_tool_choice": true + }, + "medlm-medium": { + "input_cost_per_character": 5e-07, + "litellm_provider": "vertex_ai-language-models", + "max_input_tokens": 32768, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_character": 1e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models", + "supports_tool_choice": true + }, + "meta.llama2-13b-chat-v1": { + "input_cost_per_token": 7.5e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1e-06 + }, + "meta.llama2-70b-chat-v1": { + "input_cost_per_token": 1.95e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 2.56e-06 + }, + "meta.llama3-1-405b-instruct-v1:0": { + "input_cost_per_token": 5.32e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.6e-05, + "supports_function_calling": true, + "supports_tool_choice": false + }, + "meta.llama3-1-70b-instruct-v1:0": { + "input_cost_per_token": 9.9e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 128000, + "max_output_tokens": 2048, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 9.9e-07, + "supports_function_calling": true, + "supports_tool_choice": false + }, + "meta.llama3-1-8b-instruct-v1:0": { + "input_cost_per_token": 2.2e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 128000, + "max_output_tokens": 2048, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 2.2e-07, + "supports_function_calling": true, + "supports_tool_choice": false + }, + "meta.llama3-2-11b-instruct-v1:0": { + "input_cost_per_token": 3.5e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 3.5e-07, + "supports_function_calling": true, + "supports_tool_choice": false, + "supports_vision": true + }, + "meta.llama3-2-1b-instruct-v1:0": { + "input_cost_per_token": 1e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-07, + "supports_function_calling": true, + "supports_tool_choice": false + }, + "meta.llama3-2-3b-instruct-v1:0": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.5e-07, + "supports_function_calling": true, + "supports_tool_choice": false + }, + "meta.llama3-2-90b-instruct-v1:0": { + "input_cost_per_token": 2e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 2e-06, + "supports_function_calling": true, + "supports_tool_choice": false, + "supports_vision": true + }, + "meta.llama3-3-70b-instruct-v1:0": { + "input_cost_per_token": 7.2e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 7.2e-07, + "supports_function_calling": true, + "supports_tool_choice": false + }, + "meta.llama3-70b-instruct-v1:0": { + "input_cost_per_token": 2.65e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 3.5e-06 + }, + "meta.llama3-8b-instruct-v1:0": { + "input_cost_per_token": 3e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 6e-07 + }, + "meta.llama4-maverick-17b-instruct-v1:0": { + "input_cost_per_token": 2.4e-07, + "input_cost_per_token_batches": 1.2e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 9.7e-07, + "output_cost_per_token_batches": 4.85e-07, + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "code" + ], + "supports_function_calling": true, + "supports_tool_choice": false + }, + "meta.llama4-scout-17b-instruct-v1:0": { + "input_cost_per_token": 1.7e-07, + "input_cost_per_token_batches": 8.5e-08, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 6.6e-07, + "output_cost_per_token_batches": 3.3e-07, + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "code" + ], + "supports_function_calling": true, + "supports_tool_choice": false + }, + "meta_llama/Llama-3.3-70B-Instruct": { + "litellm_provider": "meta_llama", + "max_input_tokens": 128000, + "max_output_tokens": 4028, + "max_tokens": 128000, + "mode": "chat", + "source": "https://llama.developer.meta.com/docs/models", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_tool_choice": true + }, + "meta_llama/Llama-3.3-8B-Instruct": { + "litellm_provider": "meta_llama", + "max_input_tokens": 128000, + "max_output_tokens": 4028, + "max_tokens": 128000, + "mode": "chat", + "source": "https://llama.developer.meta.com/docs/models", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_tool_choice": true + }, + "meta_llama/Llama-4-Maverick-17B-128E-Instruct-FP8": { + "litellm_provider": "meta_llama", + "max_input_tokens": 1000000, + "max_output_tokens": 4028, + "max_tokens": 128000, + "mode": "chat", + "source": "https://llama.developer.meta.com/docs/models", + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_tool_choice": true + }, + "meta_llama/Llama-4-Scout-17B-16E-Instruct-FP8": { + "litellm_provider": "meta_llama", + "max_input_tokens": 10000000, + "max_output_tokens": 4028, + "max_tokens": 128000, + "mode": "chat", + "source": "https://llama.developer.meta.com/docs/models", + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_tool_choice": true + }, + "minimax.minimax-m2": { + "input_cost_per_token": 3e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "supports_system_messages": true + }, + "mistral.magistral-small-2509": { + "input_cost_per_token": 5e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_system_messages": true + }, + "mistral.ministral-3-14b-instruct": { + "input_cost_per_token": 2e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2e-07, + "supports_function_calling": true, + "supports_system_messages": true + }, + "mistral.ministral-3-3b-instruct": { + "input_cost_per_token": 1e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1e-07, + "supports_function_calling": true, + "supports_system_messages": true + }, + "mistral.ministral-3-8b-instruct": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.5e-07, + "supports_function_calling": true, + "supports_system_messages": true + }, + "mistral.mistral-7b-instruct-v0:2": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2e-07, + "supports_tool_choice": true + }, + "mistral.mistral-large-2402-v1:0": { + "input_cost_per_token": 8e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2.4e-05, + "supports_function_calling": true + }, + "mistral.mistral-large-2407-v1:0": { + "input_cost_per_token": 3e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 128000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 9e-06, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "mistral.mistral-large-3-675b-instruct": { + "input_cost_per_token": 5e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "supports_function_calling": true, + "supports_system_messages": true + }, + "mistral.mistral-small-2402-v1:0": { + "input_cost_per_token": 1e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 3e-06, + "supports_function_calling": true + }, + "mistral.mixtral-8x7b-instruct-v0:1": { + "input_cost_per_token": 4.5e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 7e-07, + "supports_tool_choice": true + }, + "mistral.voxtral-mini-3b-2507": { + "input_cost_per_token": 4e-08, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 4e-08, + "supports_audio_input": true, + "supports_system_messages": true + }, + "mistral.voxtral-small-24b-2507": { + "input_cost_per_token": 1e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 3e-07, + "supports_audio_input": true, + "supports_system_messages": true + }, + "mistral/codestral-2405": { + "input_cost_per_token": 1e-06, + "litellm_provider": "mistral", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 3e-06, + "supports_assistant_prefill": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/codestral-2508": { + "input_cost_per_token": 3e-07, + "litellm_provider": "mistral", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 9e-07, + "source": "https://mistral.ai/news/codestral-25-08", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/codestral-latest": { + "input_cost_per_token": 1e-06, + "litellm_provider": "mistral", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 3e-06, + "supports_assistant_prefill": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/codestral-mamba-latest": { + "input_cost_per_token": 2.5e-07, + "litellm_provider": "mistral", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 2.5e-07, + "source": "https://mistral.ai/technology/", + "supports_assistant_prefill": true, + "supports_tool_choice": true + }, + "mistral/devstral-medium-2507": { + "input_cost_per_token": 4e-07, + "litellm_provider": "mistral", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 2e-06, + "source": "https://mistral.ai/news/devstral", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/devstral-small-2505": { + "input_cost_per_token": 1e-07, + "litellm_provider": "mistral", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 3e-07, + "source": "https://mistral.ai/news/devstral", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/devstral-small-2507": { + "input_cost_per_token": 1e-07, + "litellm_provider": "mistral", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 3e-07, + "source": "https://mistral.ai/news/devstral", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/labs-devstral-small-2512": { + "input_cost_per_token": 1e-07, + "litellm_provider": "mistral", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 3e-07, + "source": "https://docs.mistral.ai/models/devstral-small-2-25-12", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/devstral-2512": { + "input_cost_per_token": 4e-07, + "litellm_provider": "mistral", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 2e-06, + "source": "https://mistral.ai/news/devstral-2-vibe-cli", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/magistral-medium-2506": { + "input_cost_per_token": 2e-06, + "litellm_provider": "mistral", + "max_input_tokens": 40000, + "max_output_tokens": 40000, + "max_tokens": 40000, + "mode": "chat", + "output_cost_per_token": 5e-06, + "source": "https://mistral.ai/news/magistral", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/magistral-medium-2509": { + "input_cost_per_token": 2e-06, + "litellm_provider": "mistral", + "max_input_tokens": 40000, + "max_output_tokens": 40000, + "max_tokens": 40000, + "mode": "chat", + "output_cost_per_token": 5e-06, + "source": "https://mistral.ai/news/magistral", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/mistral-ocr-latest": { + "litellm_provider": "mistral", + "ocr_cost_per_page": 1e-3, + "annotation_cost_per_page": 3e-3, + "mode": "ocr", + "supported_endpoints": [ + "/v1/ocr" + ], + "source": "https://mistral.ai/pricing#api-pricing" + }, + "mistral/mistral-ocr-2505-completion": { + "litellm_provider": "mistral", + "ocr_cost_per_page": 1e-3, + "annotation_cost_per_page": 3e-3, + "mode": "ocr", + "supported_endpoints": [ + "/v1/ocr" + ], + "source": "https://mistral.ai/pricing#api-pricing" + }, + "mistral/magistral-medium-latest": { + "input_cost_per_token": 2e-06, + "litellm_provider": "mistral", + "max_input_tokens": 40000, + "max_output_tokens": 40000, + "max_tokens": 40000, + "mode": "chat", + "output_cost_per_token": 5e-06, + "source": "https://mistral.ai/news/magistral", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/magistral-small-2506": { + "input_cost_per_token": 5e-07, + "litellm_provider": "mistral", + "max_input_tokens": 40000, + "max_output_tokens": 40000, + "max_tokens": 40000, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "source": "https://mistral.ai/pricing#api-pricing", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/magistral-small-latest": { + "input_cost_per_token": 5e-07, + "litellm_provider": "mistral", + "max_input_tokens": 40000, + "max_output_tokens": 40000, + "max_tokens": 40000, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "source": "https://mistral.ai/pricing#api-pricing", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/mistral-embed": { + "input_cost_per_token": 1e-07, + "litellm_provider": "mistral", + "max_input_tokens": 8192, + "max_tokens": 8192, + "mode": "embedding" + }, + "mistral/codestral-embed": { + "input_cost_per_token": 0.15e-06, + "litellm_provider": "mistral", + "max_input_tokens": 8192, + "max_tokens": 8192, + "mode": "embedding" + }, + "mistral/codestral-embed-2505": { + "input_cost_per_token": 0.15e-06, + "litellm_provider": "mistral", + "max_input_tokens": 8192, + "max_tokens": 8192, + "mode": "embedding" + }, + "mistral/mistral-large-2402": { + "input_cost_per_token": 4e-06, + "litellm_provider": "mistral", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 1.2e-05, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/mistral-large-2407": { + "input_cost_per_token": 3e-06, + "litellm_provider": "mistral", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 9e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/mistral-large-2411": { + "input_cost_per_token": 2e-06, + "litellm_provider": "mistral", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 6e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/mistral-large-latest": { + "input_cost_per_token": 2e-06, + "litellm_provider": "mistral", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 6e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/mistral-large-3": { + "input_cost_per_token": 5e-07, + "litellm_provider": "mistral", + "max_input_tokens": 256000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "source": "https://docs.mistral.ai/models/mistral-large-3-25-12", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "mistral/mistral-medium": { + "input_cost_per_token": 2.7e-06, + "litellm_provider": "mistral", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 8.1e-06, + "supports_assistant_prefill": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/mistral-medium-2312": { + "input_cost_per_token": 2.7e-06, + "litellm_provider": "mistral", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 8.1e-06, + "supports_assistant_prefill": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/mistral-medium-2505": { + "input_cost_per_token": 4e-07, + "litellm_provider": "mistral", + "max_input_tokens": 131072, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/mistral-medium-latest": { + "input_cost_per_token": 4e-07, + "litellm_provider": "mistral", + "max_input_tokens": 131072, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/mistral-small": { + "input_cost_per_token": 1e-07, + "litellm_provider": "mistral", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 3e-07, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/mistral-small-latest": { + "input_cost_per_token": 1e-07, + "litellm_provider": "mistral", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 3e-07, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/mistral-tiny": { + "input_cost_per_token": 2.5e-07, + "litellm_provider": "mistral", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2.5e-07, + "supports_assistant_prefill": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/open-codestral-mamba": { + "input_cost_per_token": 2.5e-07, + "litellm_provider": "mistral", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 2.5e-07, + "source": "https://mistral.ai/technology/", + "supports_assistant_prefill": true, + "supports_tool_choice": true + }, + "mistral/open-mistral-7b": { + "input_cost_per_token": 2.5e-07, + "litellm_provider": "mistral", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2.5e-07, + "supports_assistant_prefill": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/open-mistral-nemo": { + "input_cost_per_token": 3e-07, + "litellm_provider": "mistral", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 3e-07, + "source": "https://mistral.ai/technology/", + "supports_assistant_prefill": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/open-mistral-nemo-2407": { + "input_cost_per_token": 3e-07, + "litellm_provider": "mistral", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 3e-07, + "source": "https://mistral.ai/technology/", + "supports_assistant_prefill": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/open-mixtral-8x22b": { + "input_cost_per_token": 2e-06, + "litellm_provider": "mistral", + "max_input_tokens": 65336, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 6e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/open-mixtral-8x7b": { + "input_cost_per_token": 7e-07, + "litellm_provider": "mistral", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 7e-07, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "mistral/pixtral-12b-2409": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "mistral", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.5e-07, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "mistral/pixtral-large-2411": { + "input_cost_per_token": 2e-06, + "litellm_provider": "mistral", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 6e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "mistral/pixtral-large-latest": { + "input_cost_per_token": 2e-06, + "litellm_provider": "mistral", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 6e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "moonshot.kimi-k2-thinking": { + "input_cost_per_token": 6e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2.5e-06, + "supports_reasoning": true, + "supports_system_messages": true + }, + "moonshot/kimi-k2-0711-preview": { + "cache_read_input_token_cost": 1.5e-07, + "input_cost_per_token": 6e-07, + "litellm_provider": "moonshot", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2.5e-06, + "source": "https://platform.moonshot.ai/docs/pricing/chat#generation-model-kimi-k2", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "moonshot/kimi-k2-0905-preview": { + "cache_read_input_token_cost": 1.5e-07, + "input_cost_per_token": 6e-07, + "litellm_provider": "moonshot", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 2.5e-06, + "source": "https://platform.moonshot.ai/docs/pricing/chat#generation-model-kimi-k2", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "moonshot/kimi-k2-turbo-preview": { + "cache_read_input_token_cost": 1.5e-07, + "input_cost_per_token": 1.15e-06, + "litellm_provider": "moonshot", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 8e-06, + "source": "https://platform.moonshot.ai/docs/pricing/chat#generation-model-kimi-k2", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "moonshot/kimi-latest": { + "cache_read_input_token_cost": 1.5e-07, + "input_cost_per_token": 2e-06, + "litellm_provider": "moonshot", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 5e-06, + "source": "https://platform.moonshot.ai/docs/pricing", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "moonshot/kimi-latest-128k": { + "cache_read_input_token_cost": 1.5e-07, + "input_cost_per_token": 2e-06, + "litellm_provider": "moonshot", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 5e-06, + "source": "https://platform.moonshot.ai/docs/pricing", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "moonshot/kimi-latest-32k": { + "cache_read_input_token_cost": 1.5e-07, + "input_cost_per_token": 1e-06, + "litellm_provider": "moonshot", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 3e-06, + "source": "https://platform.moonshot.ai/docs/pricing", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "moonshot/kimi-latest-8k": { + "cache_read_input_token_cost": 1.5e-07, + "input_cost_per_token": 2e-07, + "litellm_provider": "moonshot", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2e-06, + "source": "https://platform.moonshot.ai/docs/pricing", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "moonshot/kimi-thinking-preview": { + "cache_read_input_token_cost": 1.5e-07, + "input_cost_per_token": 6e-07, + "litellm_provider": "moonshot", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2.5e-06, + "source": "https://platform.moonshot.ai/docs/pricing/chat#generation-model-kimi-k2", + "supports_vision": true + }, + "moonshot/kimi-k2-thinking": { + "cache_read_input_token_cost": 1.5e-7, + "input_cost_per_token": 6e-7, + "litellm_provider": "moonshot", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 2.5e-6, + "source": "https://platform.moonshot.ai/docs/pricing/chat#generation-model-kimi-k2", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "moonshot/kimi-k2-thinking-turbo": { + "cache_read_input_token_cost": 1.5e-7, + "input_cost_per_token": 1.15e-6, + "litellm_provider": "moonshot", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 8e-6, + "source": "https://platform.moonshot.ai/docs/pricing/chat#generation-model-kimi-k2", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "moonshot/moonshot-v1-128k": { + "input_cost_per_token": 2e-06, + "litellm_provider": "moonshot", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 5e-06, + "source": "https://platform.moonshot.ai/docs/pricing", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "moonshot/moonshot-v1-128k-0430": { + "input_cost_per_token": 2e-06, + "litellm_provider": "moonshot", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 5e-06, + "source": "https://platform.moonshot.ai/docs/pricing", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "moonshot/moonshot-v1-128k-vision-preview": { + "input_cost_per_token": 2e-06, + "litellm_provider": "moonshot", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 5e-06, + "source": "https://platform.moonshot.ai/docs/pricing", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "moonshot/moonshot-v1-32k": { + "input_cost_per_token": 1e-06, + "litellm_provider": "moonshot", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 3e-06, + "source": "https://platform.moonshot.ai/docs/pricing", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "moonshot/moonshot-v1-32k-0430": { + "input_cost_per_token": 1e-06, + "litellm_provider": "moonshot", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 3e-06, + "source": "https://platform.moonshot.ai/docs/pricing", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "moonshot/moonshot-v1-32k-vision-preview": { + "input_cost_per_token": 1e-06, + "litellm_provider": "moonshot", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 3e-06, + "source": "https://platform.moonshot.ai/docs/pricing", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "moonshot/moonshot-v1-8k": { + "input_cost_per_token": 2e-07, + "litellm_provider": "moonshot", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2e-06, + "source": "https://platform.moonshot.ai/docs/pricing", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "moonshot/moonshot-v1-8k-0430": { + "input_cost_per_token": 2e-07, + "litellm_provider": "moonshot", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2e-06, + "source": "https://platform.moonshot.ai/docs/pricing", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "moonshot/moonshot-v1-8k-vision-preview": { + "input_cost_per_token": 2e-07, + "litellm_provider": "moonshot", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2e-06, + "source": "https://platform.moonshot.ai/docs/pricing", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "moonshot/moonshot-v1-auto": { + "input_cost_per_token": 2e-06, + "litellm_provider": "moonshot", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 5e-06, + "source": "https://platform.moonshot.ai/docs/pricing", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "morph/morph-v3-fast": { + "input_cost_per_token": 8e-07, + "litellm_provider": "morph", + "max_input_tokens": 16000, + "max_output_tokens": 16000, + "max_tokens": 16000, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "supports_function_calling": false, + "supports_parallel_function_calling": false, + "supports_system_messages": true, + "supports_tool_choice": false, + "supports_vision": false + }, + "morph/morph-v3-large": { + "input_cost_per_token": 9e-07, + "litellm_provider": "morph", + "max_input_tokens": 16000, + "max_output_tokens": 16000, + "max_tokens": 16000, + "mode": "chat", + "output_cost_per_token": 1.9e-06, + "supports_function_calling": false, + "supports_parallel_function_calling": false, + "supports_system_messages": true, + "supports_tool_choice": false, + "supports_vision": false + }, + "multimodalembedding": { + "input_cost_per_character": 2e-07, + "input_cost_per_image": 0.0001, + "input_cost_per_token": 8e-07, + "input_cost_per_video_per_second": 0.0005, + "input_cost_per_video_per_second_above_15s_interval": 0.002, + "input_cost_per_video_per_second_above_8s_interval": 0.001, + "litellm_provider": "vertex_ai-embedding-models", + "max_input_tokens": 2048, + "max_tokens": 2048, + "mode": "embedding", + "output_cost_per_token": 0, + "output_vector_size": 768, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models", + "supported_endpoints": [ + "/v1/embeddings" + ], + "supported_modalities": [ + "text", + "image", + "video" + ] + }, + "multimodalembedding@001": { + "input_cost_per_character": 2e-07, + "input_cost_per_image": 0.0001, + "input_cost_per_token": 8e-07, + "input_cost_per_video_per_second": 0.0005, + "input_cost_per_video_per_second_above_15s_interval": 0.002, + "input_cost_per_video_per_second_above_8s_interval": 0.001, + "litellm_provider": "vertex_ai-embedding-models", + "max_input_tokens": 2048, + "max_tokens": 2048, + "mode": "embedding", + "output_cost_per_token": 0, + "output_vector_size": 768, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models", + "supported_endpoints": [ + "/v1/embeddings" + ], + "supported_modalities": [ + "text", + "image", + "video" + ] + }, + "nscale/Qwen/QwQ-32B": { + "input_cost_per_token": 1.8e-07, + "litellm_provider": "nscale", + "mode": "chat", + "output_cost_per_token": 2e-07, + "source": "https://docs.nscale.com/docs/inference/serverless-models/current#chat-models" + }, + "nscale/Qwen/Qwen2.5-Coder-32B-Instruct": { + "input_cost_per_token": 6e-08, + "litellm_provider": "nscale", + "mode": "chat", + "output_cost_per_token": 2e-07, + "source": "https://docs.nscale.com/docs/inference/serverless-models/current#chat-models" + }, + "nscale/Qwen/Qwen2.5-Coder-3B-Instruct": { + "input_cost_per_token": 1e-08, + "litellm_provider": "nscale", + "mode": "chat", + "output_cost_per_token": 3e-08, + "source": "https://docs.nscale.com/docs/inference/serverless-models/current#chat-models" + }, + "nscale/Qwen/Qwen2.5-Coder-7B-Instruct": { + "input_cost_per_token": 1e-08, + "litellm_provider": "nscale", + "mode": "chat", + "output_cost_per_token": 3e-08, + "source": "https://docs.nscale.com/docs/inference/serverless-models/current#chat-models" + }, + "nscale/black-forest-labs/FLUX.1-schnell": { + "input_cost_per_pixel": 1.3e-09, + "litellm_provider": "nscale", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "source": "https://docs.nscale.com/docs/inference/serverless-models/current#image-models", + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "nscale/deepseek-ai/DeepSeek-R1-Distill-Llama-70B": { + "input_cost_per_token": 3.75e-07, + "litellm_provider": "nscale", + "metadata": { + "notes": "Pricing listed as $0.75/1M tokens total. Assumed 50/50 split for input/output." + }, + "mode": "chat", + "output_cost_per_token": 3.75e-07, + "source": "https://docs.nscale.com/docs/inference/serverless-models/current#chat-models" + }, + "nscale/deepseek-ai/DeepSeek-R1-Distill-Llama-8B": { + "input_cost_per_token": 2.5e-08, + "litellm_provider": "nscale", + "metadata": { + "notes": "Pricing listed as $0.05/1M tokens total. Assumed 50/50 split for input/output." + }, + "mode": "chat", + "output_cost_per_token": 2.5e-08, + "source": "https://docs.nscale.com/docs/inference/serverless-models/current#chat-models" + }, + "nscale/deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B": { + "input_cost_per_token": 9e-08, + "litellm_provider": "nscale", + "metadata": { + "notes": "Pricing listed as $0.18/1M tokens total. Assumed 50/50 split for input/output." + }, + "mode": "chat", + "output_cost_per_token": 9e-08, + "source": "https://docs.nscale.com/docs/inference/serverless-models/current#chat-models" + }, + "nscale/deepseek-ai/DeepSeek-R1-Distill-Qwen-14B": { + "input_cost_per_token": 7e-08, + "litellm_provider": "nscale", + "metadata": { + "notes": "Pricing listed as $0.14/1M tokens total. Assumed 50/50 split for input/output." + }, + "mode": "chat", + "output_cost_per_token": 7e-08, + "source": "https://docs.nscale.com/docs/inference/serverless-models/current#chat-models" + }, + "nscale/deepseek-ai/DeepSeek-R1-Distill-Qwen-32B": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "nscale", + "metadata": { + "notes": "Pricing listed as $0.30/1M tokens total. Assumed 50/50 split for input/output." + }, + "mode": "chat", + "output_cost_per_token": 1.5e-07, + "source": "https://docs.nscale.com/docs/inference/serverless-models/current#chat-models" + }, + "nscale/deepseek-ai/DeepSeek-R1-Distill-Qwen-7B": { + "input_cost_per_token": 2e-07, + "litellm_provider": "nscale", + "metadata": { + "notes": "Pricing listed as $0.40/1M tokens total. Assumed 50/50 split for input/output." + }, + "mode": "chat", + "output_cost_per_token": 2e-07, + "source": "https://docs.nscale.com/docs/inference/serverless-models/current#chat-models" + }, + "nscale/meta-llama/Llama-3.1-8B-Instruct": { + "input_cost_per_token": 3e-08, + "litellm_provider": "nscale", + "metadata": { + "notes": "Pricing listed as $0.06/1M tokens total. Assumed 50/50 split for input/output." + }, + "mode": "chat", + "output_cost_per_token": 3e-08, + "source": "https://docs.nscale.com/docs/inference/serverless-models/current#chat-models" + }, + "nscale/meta-llama/Llama-3.3-70B-Instruct": { + "input_cost_per_token": 2e-07, + "litellm_provider": "nscale", + "metadata": { + "notes": "Pricing listed as $0.40/1M tokens total. Assumed 50/50 split for input/output." + }, + "mode": "chat", + "output_cost_per_token": 2e-07, + "source": "https://docs.nscale.com/docs/inference/serverless-models/current#chat-models" + }, + "nscale/meta-llama/Llama-4-Scout-17B-16E-Instruct": { + "input_cost_per_token": 9e-08, + "litellm_provider": "nscale", + "mode": "chat", + "output_cost_per_token": 2.9e-07, + "source": "https://docs.nscale.com/docs/inference/serverless-models/current#chat-models" + }, + "nscale/mistralai/mixtral-8x22b-instruct-v0.1": { + "input_cost_per_token": 6e-07, + "litellm_provider": "nscale", + "metadata": { + "notes": "Pricing listed as $1.20/1M tokens total. Assumed 50/50 split for input/output." + }, + "mode": "chat", + "output_cost_per_token": 6e-07, + "source": "https://docs.nscale.com/docs/inference/serverless-models/current#chat-models" + }, + "nscale/stabilityai/stable-diffusion-xl-base-1.0": { + "input_cost_per_pixel": 3e-09, + "litellm_provider": "nscale", + "mode": "image_generation", + "output_cost_per_pixel": 0.0, + "source": "https://docs.nscale.com/docs/inference/serverless-models/current#image-models", + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "nvidia.nemotron-nano-12b-v2": { + "input_cost_per_token": 2e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 6e-07, + "supports_system_messages": true, + "supports_vision": true + }, + "nvidia.nemotron-nano-9b-v2": { + "input_cost_per_token": 6e-08, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2.3e-07, + "supports_system_messages": true + }, + "o1": { + "cache_read_input_token_cost": 7.5e-06, + "input_cost_per_token": 1.5e-05, + "litellm_provider": "openai", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 6e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "o1-2024-12-17": { + "cache_read_input_token_cost": 7.5e-06, + "input_cost_per_token": 1.5e-05, + "litellm_provider": "openai", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 6e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "o1-mini": { + "cache_read_input_token_cost": 5.5e-07, + "input_cost_per_token": 1.1e-06, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 4.4e-06, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_vision": true + }, + "o1-mini-2024-09-12": { + "deprecation_date": "2025-10-27", + "cache_read_input_token_cost": 1.5e-06, + "input_cost_per_token": 3e-06, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 1.2e-05, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_vision": true + }, + "o1-preview": { + "cache_read_input_token_cost": 7.5e-06, + "input_cost_per_token": 1.5e-05, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 6e-05, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_vision": true + }, + "o1-preview-2024-09-12": { + "cache_read_input_token_cost": 7.5e-06, + "input_cost_per_token": 1.5e-05, + "litellm_provider": "openai", + "max_input_tokens": 128000, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 6e-05, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_vision": true + }, + "o1-pro": { + "input_cost_per_token": 0.00015, + "input_cost_per_token_batches": 7.5e-05, + "litellm_provider": "openai", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "responses", + "output_cost_per_token": 0.0006, + "output_cost_per_token_batches": 0.0003, + "supported_endpoints": [ + "/v1/responses", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": false, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "o1-pro-2025-03-19": { + "input_cost_per_token": 0.00015, + "input_cost_per_token_batches": 7.5e-05, + "litellm_provider": "openai", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "responses", + "output_cost_per_token": 0.0006, + "output_cost_per_token_batches": 0.0003, + "supported_endpoints": [ + "/v1/responses", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": false, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "o3": { + "cache_read_input_token_cost": 5e-07, + "cache_read_input_token_cost_flex": 2.5e-07, + "cache_read_input_token_cost_priority": 8.75e-07, + "input_cost_per_token": 2e-06, + "input_cost_per_token_flex": 1e-06, + "input_cost_per_token_priority": 3.5e-06, + "litellm_provider": "openai", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 8e-06, + "output_cost_per_token_flex": 4e-06, + "output_cost_per_token_priority": 1.4e-05, + "supported_endpoints": [ + "/v1/responses", + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": false, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "o3-2025-04-16": { + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 2e-06, + "litellm_provider": "openai", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 8e-06, + "supported_endpoints": [ + "/v1/responses", + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": false, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "o3-deep-research": { + "cache_read_input_token_cost": 2.5e-06, + "input_cost_per_token": 1e-05, + "input_cost_per_token_batches": 5e-06, + "litellm_provider": "openai", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "responses", + "output_cost_per_token": 4e-05, + "output_cost_per_token_batches": 2e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "o3-deep-research-2025-06-26": { + "cache_read_input_token_cost": 2.5e-06, + "input_cost_per_token": 1e-05, + "input_cost_per_token_batches": 5e-06, + "litellm_provider": "openai", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "responses", + "output_cost_per_token": 4e-05, + "output_cost_per_token_batches": 2e-05, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "o3-mini": { + "cache_read_input_token_cost": 5.5e-07, + "input_cost_per_token": 1.1e-06, + "litellm_provider": "openai", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 4.4e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": false, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "o3-mini-2025-01-31": { + "cache_read_input_token_cost": 5.5e-07, + "input_cost_per_token": 1.1e-06, + "litellm_provider": "openai", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 4.4e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": false, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "o3-pro": { + "input_cost_per_token": 2e-05, + "input_cost_per_token_batches": 1e-05, + "litellm_provider": "openai", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "responses", + "output_cost_per_token": 8e-05, + "output_cost_per_token_batches": 4e-05, + "supported_endpoints": [ + "/v1/responses", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": false, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "o3-pro-2025-06-10": { + "input_cost_per_token": 2e-05, + "input_cost_per_token_batches": 1e-05, + "litellm_provider": "openai", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "responses", + "output_cost_per_token": 8e-05, + "output_cost_per_token_batches": 4e-05, + "supported_endpoints": [ + "/v1/responses", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_parallel_function_calling": false, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "o4-mini": { + "cache_read_input_token_cost": 2.75e-07, + "cache_read_input_token_cost_flex": 1.375e-07, + "cache_read_input_token_cost_priority": 5e-07, + "input_cost_per_token": 1.1e-06, + "input_cost_per_token_flex": 5.5e-07, + "input_cost_per_token_priority": 2e-06, + "litellm_provider": "openai", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 4.4e-06, + "output_cost_per_token_flex": 2.2e-06, + "output_cost_per_token_priority": 8e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": false, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "o4-mini-2025-04-16": { + "cache_read_input_token_cost": 2.75e-07, + "input_cost_per_token": 1.1e-06, + "litellm_provider": "openai", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 4.4e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": false, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_service_tier": true, + "supports_vision": true + }, + "o4-mini-deep-research": { + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 2e-06, + "input_cost_per_token_batches": 1e-06, + "litellm_provider": "openai", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "responses", + "output_cost_per_token": 8e-06, + "output_cost_per_token_batches": 4e-06, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "o4-mini-deep-research-2025-06-26": { + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 2e-06, + "input_cost_per_token_batches": 1e-06, + "litellm_provider": "openai", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "responses", + "output_cost_per_token": 8e-06, + "output_cost_per_token_batches": 4e-06, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/batch", + "/v1/responses" + ], + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "oci/meta.llama-3.1-405b-instruct": { + "input_cost_per_token": 1.068e-05, + "litellm_provider": "oci", + "max_input_tokens": 128000, + "max_output_tokens": 4000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.068e-05, + "source": "https://www.oracle.com/artificial-intelligence/generative-ai/generative-ai-service/pricing", + "supports_function_calling": true, + "supports_response_schema": false + }, + "oci/meta.llama-3.2-90b-vision-instruct": { + "input_cost_per_token": 2e-06, + "litellm_provider": "oci", + "max_input_tokens": 128000, + "max_output_tokens": 4000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 2e-06, + "source": "https://www.oracle.com/artificial-intelligence/generative-ai/generative-ai-service/pricing", + "supports_function_calling": true, + "supports_response_schema": false + }, + "oci/meta.llama-3.3-70b-instruct": { + "input_cost_per_token": 7.2e-07, + "litellm_provider": "oci", + "max_input_tokens": 128000, + "max_output_tokens": 4000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 7.2e-07, + "source": "https://www.oracle.com/artificial-intelligence/generative-ai/generative-ai-service/pricing", + "supports_function_calling": true, + "supports_response_schema": false + }, + "oci/meta.llama-4-maverick-17b-128e-instruct-fp8": { + "input_cost_per_token": 7.2e-07, + "litellm_provider": "oci", + "max_input_tokens": 512000, + "max_output_tokens": 4000, + "max_tokens": 512000, + "mode": "chat", + "output_cost_per_token": 7.2e-07, + "source": "https://www.oracle.com/artificial-intelligence/generative-ai/generative-ai-service/pricing", + "supports_function_calling": true, + "supports_response_schema": false + }, + "oci/meta.llama-4-scout-17b-16e-instruct": { + "input_cost_per_token": 7.2e-07, + "litellm_provider": "oci", + "max_input_tokens": 192000, + "max_output_tokens": 4000, + "max_tokens": 192000, + "mode": "chat", + "output_cost_per_token": 7.2e-07, + "source": "https://www.oracle.com/artificial-intelligence/generative-ai/generative-ai-service/pricing", + "supports_function_calling": true, + "supports_response_schema": false + }, + "oci/xai.grok-3": { + "input_cost_per_token": 3e-06, + "litellm_provider": "oci", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1.5e-07, + "source": "https://www.oracle.com/artificial-intelligence/generative-ai/generative-ai-service/pricing", + "supports_function_calling": true, + "supports_response_schema": false + }, + "oci/xai.grok-3-fast": { + "input_cost_per_token": 5e-06, + "litellm_provider": "oci", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2.5e-05, + "source": "https://www.oracle.com/artificial-intelligence/generative-ai/generative-ai-service/pricing", + "supports_function_calling": true, + "supports_response_schema": false + }, + "oci/xai.grok-3-mini": { + "input_cost_per_token": 3e-07, + "litellm_provider": "oci", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 5e-07, + "source": "https://www.oracle.com/artificial-intelligence/generative-ai/generative-ai-service/pricing", + "supports_function_calling": true, + "supports_response_schema": false + }, + "oci/xai.grok-3-mini-fast": { + "input_cost_per_token": 6e-07, + "litellm_provider": "oci", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 4e-06, + "source": "https://www.oracle.com/artificial-intelligence/generative-ai/generative-ai-service/pricing", + "supports_function_calling": true, + "supports_response_schema": false + }, + "oci/xai.grok-4": { + "input_cost_per_token": 3e-06, + "litellm_provider": "oci", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.5e-07, + "source": "https://www.oracle.com/artificial-intelligence/generative-ai/generative-ai-service/pricing", + "supports_function_calling": true, + "supports_response_schema": false + }, + "oci/cohere.command-latest": { + "input_cost_per_token": 1.56e-06, + "litellm_provider": "oci", + "max_input_tokens": 128000, + "max_output_tokens": 4000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.56e-06, + "source": "https://www.oracle.com/cloud/ai/generative-ai/pricing/", + "supports_function_calling": true, + "supports_response_schema": false + }, + "oci/cohere.command-a-03-2025": { + "input_cost_per_token": 1.56e-06, + "litellm_provider": "oci", + "max_input_tokens": 256000, + "max_output_tokens": 4000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 1.56e-06, + "source": "https://www.oracle.com/cloud/ai/generative-ai/pricing/", + "supports_function_calling": true, + "supports_response_schema": false + }, + "oci/cohere.command-plus-latest": { + "input_cost_per_token": 1.56e-06, + "litellm_provider": "oci", + "max_input_tokens": 128000, + "max_output_tokens": 4000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.56e-06, + "source": "https://www.oracle.com/cloud/ai/generative-ai/pricing/", + "supports_function_calling": true, + "supports_response_schema": false + }, + "ollama/codegeex4": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 32768, + "max_output_tokens": 8192, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 0.0, + "supports_function_calling": false + }, + "ollama/codegemma": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "completion", + "output_cost_per_token": 0.0 + }, + "ollama/codellama": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "completion", + "output_cost_per_token": 0.0 + }, + "ollama/deepseek-coder-v2-base": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "completion", + "output_cost_per_token": 0.0, + "supports_function_calling": true + }, + "ollama/deepseek-coder-v2-instruct": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 32768, + "max_output_tokens": 8192, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 0.0, + "supports_function_calling": true + }, + "ollama/deepseek-coder-v2-lite-base": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "completion", + "output_cost_per_token": 0.0, + "supports_function_calling": true + }, + "ollama/deepseek-coder-v2-lite-instruct": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 32768, + "max_output_tokens": 8192, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 0.0, + "supports_function_calling": true + }, + "ollama/deepseek-v3.1:671b-cloud" : { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "max_tokens": 163840, + "mode": "chat", + "output_cost_per_token": 0.0, + "supports_function_calling": true + }, + "ollama/gpt-oss:120b-cloud" : { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 0.0, + "supports_function_calling": true + }, + "ollama/gpt-oss:20b-cloud" : { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 0.0, + "supports_function_calling": true + }, + "ollama/internlm2_5-20b-chat": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 32768, + "max_output_tokens": 8192, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 0.0, + "supports_function_calling": true + }, + "ollama/llama2": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 0.0 + }, + "ollama/llama2-uncensored": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "completion", + "output_cost_per_token": 0.0 + }, + "ollama/llama2:13b": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 0.0 + }, + "ollama/llama2:70b": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 0.0 + }, + "ollama/llama2:7b": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 0.0 + }, + "ollama/llama3": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 0.0 + }, + "ollama/llama3.1": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 0.0, + "supports_function_calling": true + }, + "ollama/llama3:70b": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 0.0 + }, + "ollama/llama3:8b": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 0.0 + }, + "ollama/mistral": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "completion", + "output_cost_per_token": 0.0, + "supports_function_calling": true + }, + "ollama/mistral-7B-Instruct-v0.1": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 0.0, + "supports_function_calling": true + }, + "ollama/mistral-7B-Instruct-v0.2": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 0.0, + "supports_function_calling": true + }, + "ollama/mistral-large-instruct-2407": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 65536, + "max_output_tokens": 8192, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 0.0, + "supports_function_calling": true + }, + "ollama/mixtral-8x22B-Instruct-v0.1": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 65536, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 0.0, + "supports_function_calling": true + }, + "ollama/mixtral-8x7B-Instruct-v0.1": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 0.0, + "supports_function_calling": true + }, + "ollama/orca-mini": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "completion", + "output_cost_per_token": 0.0 + }, + "ollama/qwen3-coder:480b-cloud": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 0.0, + "supports_function_calling": true + }, + "ollama/vicuna": { + "input_cost_per_token": 0.0, + "litellm_provider": "ollama", + "max_input_tokens": 2048, + "max_output_tokens": 2048, + "max_tokens": 2048, + "mode": "completion", + "output_cost_per_token": 0.0 + }, + "omni-moderation-2024-09-26": { + "input_cost_per_token": 0.0, + "litellm_provider": "openai", + "max_input_tokens": 32768, + "max_output_tokens": 0, + "max_tokens": 32768, + "mode": "moderation", + "output_cost_per_token": 0.0 + }, + "omni-moderation-latest": { + "input_cost_per_token": 0.0, + "litellm_provider": "openai", + "max_input_tokens": 32768, + "max_output_tokens": 0, + "max_tokens": 32768, + "mode": "moderation", + "output_cost_per_token": 0.0 + }, + "omni-moderation-latest-intents": { + "input_cost_per_token": 0.0, + "litellm_provider": "openai", + "max_input_tokens": 32768, + "max_output_tokens": 0, + "max_tokens": 32768, + "mode": "moderation", + "output_cost_per_token": 0.0 + }, + "openai.gpt-oss-120b-1:0": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 6e-07, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "openai.gpt-oss-20b-1:0": { + "input_cost_per_token": 7e-08, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 3e-07, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "openai.gpt-oss-safeguard-120b": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 6e-07, + "supports_system_messages": true + }, + "openai.gpt-oss-safeguard-20b": { + "input_cost_per_token": 7e-08, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2e-07, + "supports_system_messages": true + }, + "openrouter/anthropic/claude-2": { + "input_cost_per_token": 1.102e-05, + "litellm_provider": "openrouter", + "max_output_tokens": 8191, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 3.268e-05, + "supports_tool_choice": true + }, + "openrouter/anthropic/claude-3-5-haiku": { + "input_cost_per_token": 1e-06, + "litellm_provider": "openrouter", + "max_tokens": 200000, + "mode": "chat", + "output_cost_per_token": 5e-06, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "openrouter/anthropic/claude-3-5-haiku-20241022": { + "input_cost_per_token": 1e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 5e-06, + "supports_function_calling": true, + "supports_tool_choice": true, + "tool_use_system_prompt_tokens": 264 + }, + "openrouter/anthropic/claude-3-haiku": { + "input_cost_per_image": 0.0004, + "input_cost_per_token": 2.5e-07, + "litellm_provider": "openrouter", + "max_tokens": 200000, + "mode": "chat", + "output_cost_per_token": 1.25e-06, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/anthropic/claude-3-haiku-20240307": { + "input_cost_per_token": 2.5e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.25e-06, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 264 + }, + "openrouter/anthropic/claude-3-opus": { + "input_cost_per_token": 1.5e-05, + "litellm_provider": "openrouter", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 395 + }, + "openrouter/anthropic/claude-3-sonnet": { + "input_cost_per_image": 0.0048, + "input_cost_per_token": 3e-06, + "litellm_provider": "openrouter", + "max_tokens": 200000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/anthropic/claude-3.5-sonnet": { + "input_cost_per_token": 3e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "openrouter/anthropic/claude-3.5-sonnet:beta": { + "input_cost_per_token": 3e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "openrouter/anthropic/claude-3.7-sonnet": { + "input_cost_per_image": 0.0048, + "input_cost_per_token": 3e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 200000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "openrouter/anthropic/claude-3.7-sonnet:beta": { + "input_cost_per_image": 0.0048, + "input_cost_per_token": 3e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 200000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "openrouter/anthropic/claude-instant-v1": { + "input_cost_per_token": 1.63e-06, + "litellm_provider": "openrouter", + "max_output_tokens": 8191, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 5.51e-06, + "supports_tool_choice": true + }, + "openrouter/anthropic/claude-opus-4": { + "input_cost_per_image": 0.0048, + "cache_creation_input_token_cost": 1.875e-05, + "cache_read_input_token_cost": 1.5e-06, + "input_cost_per_token": 1.5e-05, + "litellm_provider": "openrouter", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "openrouter/anthropic/claude-opus-4.1": { + "input_cost_per_image": 0.0048, + "cache_creation_input_token_cost": 1.875e-05, + "cache_creation_input_token_cost_above_1hr": 3e-05, + "cache_read_input_token_cost": 1.5e-06, + "input_cost_per_token": 1.5e-05, + "litellm_provider": "openrouter", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "openrouter/anthropic/claude-sonnet-4": { + "input_cost_per_image": 0.0048, + "cache_creation_input_token_cost": 3.75e-06, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost": 3e-07, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "litellm_provider": "openrouter", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "openrouter/anthropic/claude-opus-4.5": { + "cache_creation_input_token_cost": 6.25e-06, + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 5e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 2.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "openrouter/anthropic/claude-sonnet-4.5": { + "input_cost_per_image": 0.0048, + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 1000000, + "max_output_tokens": 1000000, + "max_tokens": 1000000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "openrouter/anthropic/claude-haiku-4.5": { + "cache_creation_input_token_cost": 1.25e-06, + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 1e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 200000, + "max_output_tokens": 200000, + "max_tokens": 200000, + "mode": "chat", + "output_cost_per_token": 5e-06, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 346 + }, + "openrouter/bytedance/ui-tars-1.5-7b": { + "input_cost_per_token": 1e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 131072, + "max_output_tokens": 2048, + "max_tokens": 2048, + "mode": "chat", + "output_cost_per_token": 2e-07, + "source": "https://openrouter.ai/api/v1/models/bytedance/ui-tars-1.5-7b", + "supports_tool_choice": true + }, + "openrouter/cognitivecomputations/dolphin-mixtral-8x7b": { + "input_cost_per_token": 5e-07, + "litellm_provider": "openrouter", + "max_tokens": 32769, + "mode": "chat", + "output_cost_per_token": 5e-07, + "supports_tool_choice": true + }, + "openrouter/cohere/command-r-plus": { + "input_cost_per_token": 3e-06, + "litellm_provider": "openrouter", + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_tool_choice": true + }, + "openrouter/databricks/dbrx-instruct": { + "input_cost_per_token": 6e-07, + "litellm_provider": "openrouter", + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 6e-07, + "supports_tool_choice": true + }, + "openrouter/deepseek/deepseek-chat": { + "input_cost_per_token": 1.4e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 65536, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2.8e-07, + "supports_prompt_caching": true, + "supports_tool_choice": true + }, + "openrouter/deepseek/deepseek-chat-v3-0324": { + "input_cost_per_token": 1.4e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 65536, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2.8e-07, + "supports_prompt_caching": true, + "supports_tool_choice": true + }, + "openrouter/deepseek/deepseek-chat-v3.1": { + "input_cost_per_token": 2e-07, + "input_cost_per_token_cache_hit": 2e-08, + "litellm_provider": "openrouter", + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 8e-07, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "openrouter/deepseek/deepseek-v3.2": { + "input_cost_per_token": 2.8e-07, + "input_cost_per_token_cache_hit": 2.8e-08, + "litellm_provider": "openrouter", + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "openrouter/deepseek/deepseek-v3.2-exp": { + "input_cost_per_token": 2e-07, + "input_cost_per_token_cache_hit": 2e-08, + "litellm_provider": "openrouter", + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": false, + "supports_tool_choice": true + }, + "openrouter/deepseek/deepseek-coder": { + "input_cost_per_token": 1.4e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 66000, + "max_output_tokens": 4096, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2.8e-07, + "supports_prompt_caching": true, + "supports_tool_choice": true + }, + "openrouter/deepseek/deepseek-r1": { + "input_cost_per_token": 5.5e-07, + "input_cost_per_token_cache_hit": 1.4e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 65336, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2.19e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "openrouter/deepseek/deepseek-r1-0528": { + "input_cost_per_token": 5e-07, + "input_cost_per_token_cache_hit": 1.4e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 65336, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2.15e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "openrouter/fireworks/firellava-13b": { + "input_cost_per_token": 2e-07, + "litellm_provider": "openrouter", + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 2e-07, + "supports_tool_choice": true + }, + "openrouter/google/gemini-2.0-flash-001": { + "input_cost_per_audio_token": 7e-07, + "input_cost_per_token": 1e-07, + "litellm_provider": "openrouter", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/google/gemini-2.5-flash": { + "input_cost_per_audio_token": 7e-07, + "input_cost_per_token": 3e-07, + "litellm_provider": "openrouter", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 2.5e-06, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/google/gemini-2.5-pro": { + "input_cost_per_audio_token": 7e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "openrouter", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_pdf_size_mb": 30, + "max_tokens": 8192, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supports_audio_output": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/google/gemini-3-pro-preview": { + "cache_read_input_token_cost": 2e-07, + "cache_read_input_token_cost_above_200k_tokens": 4e-07, + "cache_creation_input_token_cost_above_200k_tokens": 2.5e-07, + "input_cost_per_token": 2e-06, + "input_cost_per_token_above_200k_tokens": 4e-06, + "input_cost_per_token_batches": 1e-06, + "litellm_provider": "openrouter", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 1.2e-05, + "output_cost_per_token_above_200k_tokens": 1.8e-05, + "output_cost_per_token_batches": 6e-06, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_input": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_video_input": true, + "supports_vision": true, + "supports_web_search": true + }, + "openrouter/google/gemini-pro-1.5": { + "input_cost_per_image": 0.00265, + "input_cost_per_token": 2.5e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 1000000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 7.5e-06, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/google/gemini-pro-vision": { + "input_cost_per_image": 0.0025, + "input_cost_per_token": 1.25e-07, + "litellm_provider": "openrouter", + "max_tokens": 45875, + "mode": "chat", + "output_cost_per_token": 3.75e-07, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/google/palm-2-chat-bison": { + "input_cost_per_token": 5e-07, + "litellm_provider": "openrouter", + "max_tokens": 25804, + "mode": "chat", + "output_cost_per_token": 5e-07, + "supports_tool_choice": true + }, + "openrouter/google/palm-2-codechat-bison": { + "input_cost_per_token": 5e-07, + "litellm_provider": "openrouter", + "max_tokens": 20070, + "mode": "chat", + "output_cost_per_token": 5e-07, + "supports_tool_choice": true + }, + "openrouter/gryphe/mythomax-l2-13b": { + "input_cost_per_token": 1.875e-06, + "litellm_provider": "openrouter", + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.875e-06, + "supports_tool_choice": true + }, + "openrouter/jondurbin/airoboros-l2-70b-2.1": { + "input_cost_per_token": 1.3875e-05, + "litellm_provider": "openrouter", + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.3875e-05, + "supports_tool_choice": true + }, + "openrouter/mancer/weaver": { + "input_cost_per_token": 5.625e-06, + "litellm_provider": "openrouter", + "max_tokens": 8000, + "mode": "chat", + "output_cost_per_token": 5.625e-06, + "supports_tool_choice": true + }, + "openrouter/meta-llama/codellama-34b-instruct": { + "input_cost_per_token": 5e-07, + "litellm_provider": "openrouter", + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 5e-07, + "supports_tool_choice": true + }, + "openrouter/meta-llama/llama-2-13b-chat": { + "input_cost_per_token": 2e-07, + "litellm_provider": "openrouter", + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 2e-07, + "supports_tool_choice": true + }, + "openrouter/meta-llama/llama-2-70b-chat": { + "input_cost_per_token": 1.5e-06, + "litellm_provider": "openrouter", + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "supports_tool_choice": true + }, + "openrouter/meta-llama/llama-3-70b-instruct": { + "input_cost_per_token": 5.9e-07, + "litellm_provider": "openrouter", + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 7.9e-07, + "supports_tool_choice": true + }, + "openrouter/meta-llama/llama-3-70b-instruct:nitro": { + "input_cost_per_token": 9e-07, + "litellm_provider": "openrouter", + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 9e-07, + "supports_tool_choice": true + }, + "openrouter/meta-llama/llama-3-8b-instruct:extended": { + "input_cost_per_token": 2.25e-07, + "litellm_provider": "openrouter", + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 2.25e-06, + "supports_tool_choice": true + }, + "openrouter/meta-llama/llama-3-8b-instruct:free": { + "input_cost_per_token": 0.0, + "litellm_provider": "openrouter", + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 0.0, + "supports_tool_choice": true + }, + "openrouter/microsoft/wizardlm-2-8x22b:nitro": { + "input_cost_per_token": 1e-06, + "litellm_provider": "openrouter", + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 1e-06, + "supports_tool_choice": true + }, + "openrouter/minimax/minimax-m2": { + "input_cost_per_token": 2.55e-7, + "litellm_provider": "openrouter", + "max_input_tokens": 204800, + "max_output_tokens": 204800, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 1.02e-6, + "supports_function_calling": true, + "supports_prompt_caching": false, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "openrouter/mistralai/devstral-2512:free": { + "input_cost_per_image": 0, + "input_cost_per_token": 0, + "litellm_provider": "openrouter", + "max_input_tokens": 262144, + "max_output_tokens": null, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 0, + "supports_function_calling": true, + "supports_prompt_caching": false, + "supports_tool_choice": true, + "supports_vision": false + }, + "openrouter/mistralai/devstral-2512": { + "input_cost_per_image": 0, + "input_cost_per_token": 1.5e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 262144, + "max_output_tokens": 65536, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 6e-07, + "supports_function_calling": true, + "supports_prompt_caching": false, + "supports_tool_choice": true, + "supports_vision": false + }, + "openrouter/mistralai/ministral-3b-2512": { + "input_cost_per_image": 0, + "input_cost_per_token": 1e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 131072, + "max_output_tokens": null, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1e-07, + "supports_function_calling": true, + "supports_prompt_caching": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/mistralai/ministral-8b-2512": { + "input_cost_per_image": 0, + "input_cost_per_token": 1.5e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 262144, + "max_output_tokens": null, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 1.5e-07, + "supports_function_calling": true, + "supports_prompt_caching": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/mistralai/ministral-14b-2512": { + "input_cost_per_image": 0, + "input_cost_per_token": 2e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 262144, + "max_output_tokens": null, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 2e-07, + "supports_function_calling": true, + "supports_prompt_caching": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/mistralai/mistral-large-2512": { + "input_cost_per_image": 0, + "input_cost_per_token": 5e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 262144, + "max_output_tokens": null, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "supports_function_calling": true, + "supports_prompt_caching": false, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/mistralai/mistral-7b-instruct": { + "input_cost_per_token": 1.3e-07, + "litellm_provider": "openrouter", + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.3e-07, + "supports_tool_choice": true + }, + "openrouter/mistralai/mistral-7b-instruct:free": { + "input_cost_per_token": 0.0, + "litellm_provider": "openrouter", + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 0.0, + "supports_tool_choice": true + }, + "openrouter/mistralai/mistral-large": { + "input_cost_per_token": 8e-06, + "litellm_provider": "openrouter", + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 2.4e-05, + "supports_tool_choice": true + }, + "openrouter/mistralai/mistral-small-3.1-24b-instruct": { + "input_cost_per_token": 1e-07, + "litellm_provider": "openrouter", + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 3e-07, + "supports_tool_choice": true + }, + "openrouter/mistralai/mistral-small-3.2-24b-instruct": { + "input_cost_per_token": 1e-07, + "litellm_provider": "openrouter", + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 3e-07, + "supports_tool_choice": true + }, + "openrouter/mistralai/mixtral-8x22b-instruct": { + "input_cost_per_token": 6.5e-07, + "litellm_provider": "openrouter", + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 6.5e-07, + "supports_tool_choice": true + }, + "openrouter/nousresearch/nous-hermes-llama2-13b": { + "input_cost_per_token": 2e-07, + "litellm_provider": "openrouter", + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 2e-07, + "supports_tool_choice": true + }, + "openrouter/openai/gpt-3.5-turbo": { + "input_cost_per_token": 1.5e-06, + "litellm_provider": "openrouter", + "max_tokens": 4095, + "mode": "chat", + "output_cost_per_token": 2e-06, + "supports_tool_choice": true + }, + "openrouter/openai/gpt-3.5-turbo-16k": { + "input_cost_per_token": 3e-06, + "litellm_provider": "openrouter", + "max_tokens": 16383, + "mode": "chat", + "output_cost_per_token": 4e-06, + "supports_tool_choice": true + }, + "openrouter/openai/gpt-4": { + "input_cost_per_token": 3e-05, + "litellm_provider": "openrouter", + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 6e-05, + "supports_tool_choice": true + }, + "openrouter/openai/gpt-4-vision-preview": { + "input_cost_per_image": 0.01445, + "input_cost_per_token": 1e-05, + "litellm_provider": "openrouter", + "max_tokens": 130000, + "mode": "chat", + "output_cost_per_token": 3e-05, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/openai/gpt-4.1": { + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 2e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 8e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/openai/gpt-4.1-2025-04-14": { + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 2e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 8e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/openai/gpt-4.1-mini": { + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 4e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 1.6e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/openai/gpt-4.1-mini-2025-04-14": { + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 4e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 1.6e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/openai/gpt-4.1-nano": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_token": 1e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/openai/gpt-4.1-nano-2025-04-14": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_token": 1e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/openai/gpt-4o": { + "input_cost_per_token": 2.5e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/openai/gpt-4o-2024-05-13": { + "input_cost_per_token": 5e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/openai/gpt-5-chat": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_reasoning": true, + "supports_tool_choice": true + }, + "openrouter/openai/gpt-5-codex": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_reasoning": true, + "supports_tool_choice": true + }, + "openrouter/openai/gpt-5": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_reasoning": true, + "supports_tool_choice": true + }, + "openrouter/openai/gpt-5-mini": { + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_token": 2.5e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 2e-06, + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_reasoning": true, + "supports_tool_choice": true + }, + "openrouter/openai/gpt-5-nano": { + "cache_read_input_token_cost": 5e-09, + "input_cost_per_token": 5e-08, + "litellm_provider": "openrouter", + "max_input_tokens": 272000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_reasoning": true, + "supports_tool_choice": true + }, + "openrouter/openai/gpt-5.2": { + "input_cost_per_image": 0, + "cache_read_input_token_cost": 1.75e-07, + "input_cost_per_token": 1.75e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 400000, + "mode": "chat", + "output_cost_per_token": 1.4e-05, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/openai/gpt-5.2-chat": { + "input_cost_per_image": 0, + "cache_read_input_token_cost": 1.75e-07, + "input_cost_per_token": 1.75e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.4e-05, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/openai/gpt-5.2-pro": { + "input_cost_per_image": 0, + "input_cost_per_token": 2.1e-05, + "litellm_provider": "openrouter", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 400000, + "mode": "chat", + "output_cost_per_token": 1.68e-04, + "supports_function_calling": true, + "supports_prompt_caching": false, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/openai/gpt-oss-120b": { + "input_cost_per_token": 1.8e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 131072, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 8e-07, + "source": "https://openrouter.ai/openai/gpt-oss-120b", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "openrouter/openai/gpt-oss-20b": { + "input_cost_per_token": 1.8e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 131072, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 8e-07, + "source": "https://openrouter.ai/openai/gpt-oss-20b", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "openrouter/openai/o1": { + "cache_read_input_token_cost": 7.5e-06, + "input_cost_per_token": 1.5e-05, + "litellm_provider": "openrouter", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 100000, + "mode": "chat", + "output_cost_per_token": 6e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/openai/o1-mini": { + "input_cost_per_token": 3e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 128000, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 1.2e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "openrouter/openai/o1-mini-2024-09-12": { + "input_cost_per_token": 3e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 128000, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 1.2e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "openrouter/openai/o1-preview": { + "input_cost_per_token": 1.5e-05, + "litellm_provider": "openrouter", + "max_input_tokens": 128000, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 6e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "openrouter/openai/o1-preview-2024-09-12": { + "input_cost_per_token": 1.5e-05, + "litellm_provider": "openrouter", + "max_input_tokens": 128000, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 6e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "openrouter/openai/o3-mini": { + "input_cost_per_token": 1.1e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 128000, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 4.4e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "openrouter/openai/o3-mini-high": { + "input_cost_per_token": 1.1e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 128000, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 4.4e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "openrouter/pygmalionai/mythalion-13b": { + "input_cost_per_token": 1.875e-06, + "litellm_provider": "openrouter", + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.875e-06, + "supports_tool_choice": true + }, + "openrouter/qwen/qwen-2.5-coder-32b-instruct": { + "input_cost_per_token": 1.8e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 33792, + "max_output_tokens": 33792, + "max_tokens": 33792, + "mode": "chat", + "output_cost_per_token": 1.8e-07, + "supports_tool_choice": true + }, + "openrouter/qwen/qwen-vl-plus": { + "input_cost_per_token": 2.1e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 8192, + "max_output_tokens": 2048, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 6.3e-07, + "supports_tool_choice": true, + "supports_vision": true + }, + "openrouter/qwen/qwen3-coder": { + "input_cost_per_token": 2.2e-7, + "litellm_provider": "openrouter", + "max_input_tokens": 262100, + "max_output_tokens": 262100, + "max_tokens": 262100, + "mode": "chat", + "output_cost_per_token": 9.5e-7, + "source": "https://openrouter.ai/qwen/qwen3-coder", + "supports_tool_choice": true, + "supports_function_calling": true + }, + "openrouter/switchpoint/router": { + "input_cost_per_token": 8.5e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 3.4e-06, + "source": "https://openrouter.ai/switchpoint/router", + "supports_tool_choice": true + }, + "openrouter/undi95/remm-slerp-l2-13b": { + "input_cost_per_token": 1.875e-06, + "litellm_provider": "openrouter", + "max_tokens": 6144, + "mode": "chat", + "output_cost_per_token": 1.875e-06, + "supports_tool_choice": true + }, + "openrouter/x-ai/grok-4": { + "input_cost_per_token": 3e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "source": "https://openrouter.ai/x-ai/grok-4", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "openrouter/x-ai/grok-4-fast:free": { + "input_cost_per_token": 0, + "litellm_provider": "openrouter", + "max_input_tokens": 2000000, + "max_output_tokens": 30000, + "max_tokens": 2000000, + "mode": "chat", + "output_cost_per_token": 0, + "source": "https://openrouter.ai/x-ai/grok-4-fast:free", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_web_search": false + }, + "openrouter/z-ai/glm-4.6": { + "input_cost_per_token": 4.0e-7, + "litellm_provider": "openrouter", + "max_input_tokens": 202800, + "max_output_tokens": 131000, + "max_tokens": 202800, + "mode": "chat", + "output_cost_per_token": 1.75e-6, + "source": "https://openrouter.ai/z-ai/glm-4.6", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "openrouter/z-ai/glm-4.6:exacto": { + "input_cost_per_token": 4.5e-7, + "litellm_provider": "openrouter", + "max_input_tokens": 202800, + "max_output_tokens": 131000, + "max_tokens": 202800, + "mode": "chat", + "output_cost_per_token": 1.9e-6, + "source": "https://openrouter.ai/z-ai/glm-4.6:exacto", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "ovhcloud/DeepSeek-R1-Distill-Llama-70B": { + "input_cost_per_token": 6.7e-07, + "litellm_provider": "ovhcloud", + "max_input_tokens": 131000, + "max_output_tokens": 131000, + "max_tokens": 131000, + "mode": "chat", + "output_cost_per_token": 6.7e-07, + "source": "https://endpoints.ai.cloud.ovh.net/models/deepseek-r1-distill-llama-70b", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "ovhcloud/Llama-3.1-8B-Instruct": { + "input_cost_per_token": 1e-07, + "litellm_provider": "ovhcloud", + "max_input_tokens": 131000, + "max_output_tokens": 131000, + "max_tokens": 131000, + "mode": "chat", + "output_cost_per_token": 1e-07, + "source": "https://endpoints.ai.cloud.ovh.net/models/llama-3-1-8b-instruct", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "ovhcloud/Meta-Llama-3_1-70B-Instruct": { + "input_cost_per_token": 6.7e-07, + "litellm_provider": "ovhcloud", + "max_input_tokens": 131000, + "max_output_tokens": 131000, + "max_tokens": 131000, + "mode": "chat", + "output_cost_per_token": 6.7e-07, + "source": "https://endpoints.ai.cloud.ovh.net/models/meta-llama-3-1-70b-instruct", + "supports_function_calling": false, + "supports_response_schema": false, + "supports_tool_choice": false + }, + "ovhcloud/Meta-Llama-3_3-70B-Instruct": { + "input_cost_per_token": 6.7e-07, + "litellm_provider": "ovhcloud", + "max_input_tokens": 131000, + "max_output_tokens": 131000, + "max_tokens": 131000, + "mode": "chat", + "output_cost_per_token": 6.7e-07, + "source": "https://endpoints.ai.cloud.ovh.net/models/meta-llama-3-3-70b-instruct", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "ovhcloud/Mistral-7B-Instruct-v0.3": { + "input_cost_per_token": 1e-07, + "litellm_provider": "ovhcloud", + "max_input_tokens": 127000, + "max_output_tokens": 127000, + "max_tokens": 127000, + "mode": "chat", + "output_cost_per_token": 1e-07, + "source": "https://endpoints.ai.cloud.ovh.net/models/mistral-7b-instruct-v0-3", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "ovhcloud/Mistral-Nemo-Instruct-2407": { + "input_cost_per_token": 1.3e-07, + "litellm_provider": "ovhcloud", + "max_input_tokens": 118000, + "max_output_tokens": 118000, + "max_tokens": 118000, + "mode": "chat", + "output_cost_per_token": 1.3e-07, + "source": "https://endpoints.ai.cloud.ovh.net/models/mistral-nemo-instruct-2407", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "ovhcloud/Mistral-Small-3.2-24B-Instruct-2506": { + "input_cost_per_token": 9e-08, + "litellm_provider": "ovhcloud", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 2.8e-07, + "source": "https://endpoints.ai.cloud.ovh.net/models/mistral-small-3-2-24b-instruct-2506", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "ovhcloud/Mixtral-8x7B-Instruct-v0.1": { + "input_cost_per_token": 6.3e-07, + "litellm_provider": "ovhcloud", + "max_input_tokens": 32000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 6.3e-07, + "source": "https://endpoints.ai.cloud.ovh.net/models/mixtral-8x7b-instruct-v0-1", + "supports_function_calling": false, + "supports_response_schema": true, + "supports_tool_choice": false + }, + "ovhcloud/Qwen2.5-Coder-32B-Instruct": { + "input_cost_per_token": 8.7e-07, + "litellm_provider": "ovhcloud", + "max_input_tokens": 32000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 8.7e-07, + "source": "https://endpoints.ai.cloud.ovh.net/models/qwen2-5-coder-32b-instruct", + "supports_function_calling": false, + "supports_response_schema": true, + "supports_tool_choice": false + }, + "ovhcloud/Qwen2.5-VL-72B-Instruct": { + "input_cost_per_token": 9.1e-07, + "litellm_provider": "ovhcloud", + "max_input_tokens": 32000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 9.1e-07, + "source": "https://endpoints.ai.cloud.ovh.net/models/qwen2-5-vl-72b-instruct", + "supports_function_calling": false, + "supports_response_schema": true, + "supports_tool_choice": false, + "supports_vision": true + }, + "ovhcloud/Qwen3-32B": { + "input_cost_per_token": 8e-08, + "litellm_provider": "ovhcloud", + "max_input_tokens": 32000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 2.3e-07, + "source": "https://endpoints.ai.cloud.ovh.net/models/qwen3-32b", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "ovhcloud/gpt-oss-120b": { + "input_cost_per_token": 8e-08, + "litellm_provider": "ovhcloud", + "max_input_tokens": 131000, + "max_output_tokens": 131000, + "max_tokens": 131000, + "mode": "chat", + "output_cost_per_token": 4e-07, + "source": "https://endpoints.ai.cloud.ovh.net/models/gpt-oss-120b", + "supports_function_calling": false, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": false + }, + "ovhcloud/gpt-oss-20b": { + "input_cost_per_token": 4e-08, + "litellm_provider": "ovhcloud", + "max_input_tokens": 131000, + "max_output_tokens": 131000, + "max_tokens": 131000, + "mode": "chat", + "output_cost_per_token": 1.5e-07, + "source": "https://endpoints.ai.cloud.ovh.net/models/gpt-oss-20b", + "supports_function_calling": false, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": false + }, + "ovhcloud/llava-v1.6-mistral-7b-hf": { + "input_cost_per_token": 2.9e-07, + "litellm_provider": "ovhcloud", + "max_input_tokens": 32000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 2.9e-07, + "source": "https://endpoints.ai.cloud.ovh.net/models/llava-next-mistral-7b", + "supports_function_calling": false, + "supports_response_schema": true, + "supports_tool_choice": false, + "supports_vision": true + }, + "ovhcloud/mamba-codestral-7B-v0.1": { + "input_cost_per_token": 1.9e-07, + "litellm_provider": "ovhcloud", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 1.9e-07, + "source": "https://endpoints.ai.cloud.ovh.net/models/mamba-codestral-7b-v0-1", + "supports_function_calling": false, + "supports_response_schema": true, + "supports_tool_choice": false + }, + "palm/chat-bison": { + "input_cost_per_token": 1.25e-07, + "litellm_provider": "palm", + "max_input_tokens": 8192, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "palm/chat-bison-001": { + "input_cost_per_token": 1.25e-07, + "litellm_provider": "palm", + "max_input_tokens": 8192, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "palm/text-bison": { + "input_cost_per_token": 1.25e-07, + "litellm_provider": "palm", + "max_input_tokens": 8192, + "max_output_tokens": 1024, + "max_tokens": 1024, + "mode": "completion", + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "palm/text-bison-001": { + "input_cost_per_token": 1.25e-07, + "litellm_provider": "palm", + "max_input_tokens": 8192, + "max_output_tokens": 1024, + "max_tokens": 1024, + "mode": "completion", + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "palm/text-bison-safety-off": { + "input_cost_per_token": 1.25e-07, + "litellm_provider": "palm", + "max_input_tokens": 8192, + "max_output_tokens": 1024, + "max_tokens": 1024, + "mode": "completion", + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "palm/text-bison-safety-recitation-off": { + "input_cost_per_token": 1.25e-07, + "litellm_provider": "palm", + "max_input_tokens": 8192, + "max_output_tokens": 1024, + "max_tokens": 1024, + "mode": "completion", + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "parallel_ai/search": { + "input_cost_per_query": 0.004, + "litellm_provider": "parallel_ai", + "mode": "search" + }, + "parallel_ai/search-pro": { + "input_cost_per_query": 0.009, + "litellm_provider": "parallel_ai", + "mode": "search" + }, + "perplexity/codellama-34b-instruct": { + "input_cost_per_token": 3.5e-07, + "litellm_provider": "perplexity", + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1.4e-06 + }, + "perplexity/codellama-70b-instruct": { + "input_cost_per_token": 7e-07, + "litellm_provider": "perplexity", + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 2.8e-06 + }, + "perplexity/llama-2-70b-chat": { + "input_cost_per_token": 7e-07, + "litellm_provider": "perplexity", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 2.8e-06 + }, + "perplexity/llama-3.1-70b-instruct": { + "input_cost_per_token": 1e-06, + "litellm_provider": "perplexity", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1e-06 + }, + "perplexity/llama-3.1-8b-instruct": { + "input_cost_per_token": 2e-07, + "litellm_provider": "perplexity", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2e-07 + }, + "perplexity/llama-3.1-sonar-huge-128k-online": { + "deprecation_date": "2025-02-22", + "input_cost_per_token": 5e-06, + "litellm_provider": "perplexity", + "max_input_tokens": 127072, + "max_output_tokens": 127072, + "max_tokens": 127072, + "mode": "chat", + "output_cost_per_token": 5e-06 + }, + "perplexity/llama-3.1-sonar-large-128k-chat": { + "deprecation_date": "2025-02-22", + "input_cost_per_token": 1e-06, + "litellm_provider": "perplexity", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1e-06 + }, + "perplexity/llama-3.1-sonar-large-128k-online": { + "deprecation_date": "2025-02-22", + "input_cost_per_token": 1e-06, + "litellm_provider": "perplexity", + "max_input_tokens": 127072, + "max_output_tokens": 127072, + "max_tokens": 127072, + "mode": "chat", + "output_cost_per_token": 1e-06 + }, + "perplexity/llama-3.1-sonar-small-128k-chat": { + "deprecation_date": "2025-02-22", + "input_cost_per_token": 2e-07, + "litellm_provider": "perplexity", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2e-07 + }, + "perplexity/llama-3.1-sonar-small-128k-online": { + "deprecation_date": "2025-02-22", + "input_cost_per_token": 2e-07, + "litellm_provider": "perplexity", + "max_input_tokens": 127072, + "max_output_tokens": 127072, + "max_tokens": 127072, + "mode": "chat", + "output_cost_per_token": 2e-07 + }, + "perplexity/mistral-7b-instruct": { + "input_cost_per_token": 7e-08, + "litellm_provider": "perplexity", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 2.8e-07 + }, + "perplexity/mixtral-8x7b-instruct": { + "input_cost_per_token": 7e-08, + "litellm_provider": "perplexity", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 2.8e-07 + }, + "perplexity/pplx-70b-chat": { + "input_cost_per_token": 7e-07, + "litellm_provider": "perplexity", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 2.8e-06 + }, + "perplexity/pplx-70b-online": { + "input_cost_per_request": 0.005, + "input_cost_per_token": 0.0, + "litellm_provider": "perplexity", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 2.8e-06 + }, + "perplexity/pplx-7b-chat": { + "input_cost_per_token": 7e-08, + "litellm_provider": "perplexity", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2.8e-07 + }, + "perplexity/pplx-7b-online": { + "input_cost_per_request": 0.005, + "input_cost_per_token": 0.0, + "litellm_provider": "perplexity", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 2.8e-07 + }, + "perplexity/sonar": { + "input_cost_per_token": 1e-06, + "litellm_provider": "perplexity", + "max_input_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-06, + "search_context_cost_per_query": { + "search_context_size_high": 0.012, + "search_context_size_low": 0.005, + "search_context_size_medium": 0.008 + }, + "supports_web_search": true + }, + "perplexity/sonar-deep-research": { + "citation_cost_per_token": 2e-06, + "input_cost_per_token": 2e-06, + "litellm_provider": "perplexity", + "max_input_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_reasoning_token": 3e-06, + "output_cost_per_token": 8e-06, + "search_context_cost_per_query": { + "search_context_size_high": 0.005, + "search_context_size_low": 0.005, + "search_context_size_medium": 0.005 + }, + "supports_reasoning": true, + "supports_web_search": true + }, + "perplexity/sonar-medium-chat": { + "input_cost_per_token": 6e-07, + "litellm_provider": "perplexity", + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1.8e-06 + }, + "perplexity/sonar-medium-online": { + "input_cost_per_request": 0.005, + "input_cost_per_token": 0, + "litellm_provider": "perplexity", + "max_input_tokens": 12000, + "max_output_tokens": 12000, + "max_tokens": 12000, + "mode": "chat", + "output_cost_per_token": 1.8e-06 + }, + "perplexity/sonar-pro": { + "input_cost_per_token": 3e-06, + "litellm_provider": "perplexity", + "max_input_tokens": 200000, + "max_output_tokens": 8000, + "max_tokens": 8000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.014, + "search_context_size_low": 0.006, + "search_context_size_medium": 0.01 + }, + "supports_web_search": true + }, + "perplexity/sonar-reasoning": { + "input_cost_per_token": 1e-06, + "litellm_provider": "perplexity", + "max_input_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 5e-06, + "search_context_cost_per_query": { + "search_context_size_high": 0.014, + "search_context_size_low": 0.005, + "search_context_size_medium": 0.008 + }, + "supports_reasoning": true, + "supports_web_search": true + }, + "perplexity/sonar-reasoning-pro": { + "input_cost_per_token": 2e-06, + "litellm_provider": "perplexity", + "max_input_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 8e-06, + "search_context_cost_per_query": { + "search_context_size_high": 0.014, + "search_context_size_low": 0.006, + "search_context_size_medium": 0.01 + }, + "supports_reasoning": true, + "supports_web_search": true + }, + "perplexity/sonar-small-chat": { + "input_cost_per_token": 7e-08, + "litellm_provider": "perplexity", + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 2.8e-07 + }, + "perplexity/sonar-small-online": { + "input_cost_per_request": 0.005, + "input_cost_per_token": 0, + "litellm_provider": "perplexity", + "max_input_tokens": 12000, + "max_output_tokens": 12000, + "max_tokens": 12000, + "mode": "chat", + "output_cost_per_token": 2.8e-07 + }, + "publicai/swiss-ai/apertus-8b-instruct": { + "input_cost_per_token": 0.0, + "litellm_provider": "publicai", + "max_input_tokens": 8192, + "max_output_tokens": 4096, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 0.0, + "source": "https://platform.publicai.co/docs", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "publicai/swiss-ai/apertus-70b-instruct": { + "input_cost_per_token": 0.0, + "litellm_provider": "publicai", + "max_input_tokens": 8192, + "max_output_tokens": 4096, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 0.0, + "source": "https://platform.publicai.co/docs", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "publicai/aisingapore/Gemma-SEA-LION-v4-27B-IT": { + "input_cost_per_token": 0.0, + "litellm_provider": "publicai", + "max_input_tokens": 8192, + "max_output_tokens": 4096, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 0.0, + "source": "https://platform.publicai.co/docs", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "publicai/BSC-LT/salamandra-7b-instruct-tools-16k": { + "input_cost_per_token": 0.0, + "litellm_provider": "publicai", + "max_input_tokens": 16384, + "max_output_tokens": 4096, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 0.0, + "source": "https://platform.publicai.co/docs", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "publicai/BSC-LT/ALIA-40b-instruct_Q8_0": { + "input_cost_per_token": 0.0, + "litellm_provider": "publicai", + "max_input_tokens": 8192, + "max_output_tokens": 4096, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 0.0, + "source": "https://platform.publicai.co/docs", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "publicai/allenai/Olmo-3-7B-Instruct": { + "input_cost_per_token": 0.0, + "litellm_provider": "publicai", + "max_input_tokens": 32768, + "max_output_tokens": 4096, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 0.0, + "source": "https://platform.publicai.co/docs", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "publicai/aisingapore/Qwen-SEA-LION-v4-32B-IT": { + "input_cost_per_token": 0.0, + "litellm_provider": "publicai", + "max_input_tokens": 32768, + "max_output_tokens": 4096, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 0.0, + "source": "https://platform.publicai.co/docs", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "publicai/allenai/Olmo-3-7B-Think": { + "input_cost_per_token": 0.0, + "litellm_provider": "publicai", + "max_input_tokens": 32768, + "max_output_tokens": 4096, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 0.0, + "source": "https://platform.publicai.co/docs", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_reasoning": true + }, + "publicai/allenai/Olmo-3-32B-Think": { + "input_cost_per_token": 0.0, + "litellm_provider": "publicai", + "max_input_tokens": 32768, + "max_output_tokens": 4096, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 0.0, + "source": "https://platform.publicai.co/docs", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_reasoning": true + }, + "qwen.qwen3-coder-480b-a35b-v1:0": { + "input_cost_per_token": 2.2e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 262000, + "max_output_tokens": 65536, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 1.8e-06, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "qwen.qwen3-235b-a22b-2507-v1:0": { + "input_cost_per_token": 2.2e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 262144, + "max_output_tokens": 131072, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 8.8e-07, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "qwen.qwen3-coder-30b-a3b-v1:0": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 262144, + "max_output_tokens": 131072, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 6.0e-07, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "qwen.qwen3-32b-v1:0": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 131072, + "max_output_tokens": 16384, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 6.0e-07, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "qwen.qwen3-next-80b-a3b": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "supports_function_calling": true, + "supports_system_messages": true + }, + "qwen.qwen3-vl-235b-a22b": { + "input_cost_per_token": 5.3e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2.66e-06, + "supports_function_calling": true, + "supports_system_messages": true, + "supports_vision": true + }, + "recraft/recraftv2": { + "litellm_provider": "recraft", + "mode": "image_generation", + "output_cost_per_image": 0.022, + "source": "https://www.recraft.ai/docs#pricing", + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "recraft/recraftv3": { + "litellm_provider": "recraft", + "mode": "image_generation", + "output_cost_per_image": 0.04, + "source": "https://www.recraft.ai/docs#pricing", + "supported_endpoints": [ + "/v1/images/generations" + ] + }, + "replicate/meta/llama-2-13b": { + "input_cost_per_token": 1e-07, + "litellm_provider": "replicate", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 5e-07, + "supports_tool_choice": true + }, + "replicate/meta/llama-2-13b-chat": { + "input_cost_per_token": 1e-07, + "litellm_provider": "replicate", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 5e-07, + "supports_tool_choice": true + }, + "replicate/meta/llama-2-70b": { + "input_cost_per_token": 6.5e-07, + "litellm_provider": "replicate", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 2.75e-06, + "supports_tool_choice": true + }, + "replicate/meta/llama-2-70b-chat": { + "input_cost_per_token": 6.5e-07, + "litellm_provider": "replicate", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 2.75e-06, + "supports_tool_choice": true + }, + "replicate/meta/llama-2-7b": { + "input_cost_per_token": 5e-08, + "litellm_provider": "replicate", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 2.5e-07, + "supports_tool_choice": true + }, + "replicate/meta/llama-2-7b-chat": { + "input_cost_per_token": 5e-08, + "litellm_provider": "replicate", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 2.5e-07, + "supports_tool_choice": true + }, + "replicate/meta/llama-3-70b": { + "input_cost_per_token": 6.5e-07, + "litellm_provider": "replicate", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2.75e-06, + "supports_tool_choice": true + }, + "replicate/meta/llama-3-70b-instruct": { + "input_cost_per_token": 6.5e-07, + "litellm_provider": "replicate", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2.75e-06, + "supports_tool_choice": true + }, + "replicate/meta/llama-3-8b": { + "input_cost_per_token": 5e-08, + "litellm_provider": "replicate", + "max_input_tokens": 8086, + "max_output_tokens": 8086, + "max_tokens": 8086, + "mode": "chat", + "output_cost_per_token": 2.5e-07, + "supports_tool_choice": true + }, + "replicate/meta/llama-3-8b-instruct": { + "input_cost_per_token": 5e-08, + "litellm_provider": "replicate", + "max_input_tokens": 8086, + "max_output_tokens": 8086, + "max_tokens": 8086, + "mode": "chat", + "output_cost_per_token": 2.5e-07, + "supports_tool_choice": true + }, + "replicate/mistralai/mistral-7b-instruct-v0.2": { + "input_cost_per_token": 5e-08, + "litellm_provider": "replicate", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 2.5e-07, + "supports_tool_choice": true + }, + "replicate/mistralai/mistral-7b-v0.1": { + "input_cost_per_token": 5e-08, + "litellm_provider": "replicate", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 2.5e-07, + "supports_tool_choice": true + }, + "replicate/mistralai/mixtral-8x7b-instruct-v0.1": { + "input_cost_per_token": 3e-07, + "litellm_provider": "replicate", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1e-06, + "supports_tool_choice": true + }, + "rerank-english-v2.0": { + "input_cost_per_query": 0.002, + "input_cost_per_token": 0.0, + "litellm_provider": "cohere", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_query_tokens": 2048, + "max_tokens": 4096, + "mode": "rerank", + "output_cost_per_token": 0.0 + }, + "rerank-english-v3.0": { + "input_cost_per_query": 0.002, + "input_cost_per_token": 0.0, + "litellm_provider": "cohere", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_query_tokens": 2048, + "max_tokens": 4096, + "mode": "rerank", + "output_cost_per_token": 0.0 + }, + "rerank-multilingual-v2.0": { + "input_cost_per_query": 0.002, + "input_cost_per_token": 0.0, + "litellm_provider": "cohere", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_query_tokens": 2048, + "max_tokens": 4096, + "mode": "rerank", + "output_cost_per_token": 0.0 + }, + "rerank-multilingual-v3.0": { + "input_cost_per_query": 0.002, + "input_cost_per_token": 0.0, + "litellm_provider": "cohere", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_query_tokens": 2048, + "max_tokens": 4096, + "mode": "rerank", + "output_cost_per_token": 0.0 + }, + "rerank-v3.5": { + "input_cost_per_query": 0.002, + "input_cost_per_token": 0.0, + "litellm_provider": "cohere", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_query_tokens": 2048, + "max_tokens": 4096, + "mode": "rerank", + "output_cost_per_token": 0.0 + }, + "nvidia_nim/nvidia/nv-rerankqa-mistral-4b-v3": { + "input_cost_per_query": 0.0, + "input_cost_per_token": 0.0, + "litellm_provider": "nvidia_nim", + "mode": "rerank", + "output_cost_per_token": 0.0 + }, + "nvidia_nim/nvidia/llama-3_2-nv-rerankqa-1b-v2": { + "input_cost_per_query": 0.0, + "input_cost_per_token": 0.0, + "litellm_provider": "nvidia_nim", + "mode": "rerank", + "output_cost_per_token": 0.0 + }, + "nvidia_nim/ranking/nvidia/llama-3.2-nv-rerankqa-1b-v2": { + "input_cost_per_query": 0.0, + "input_cost_per_token": 0.0, + "litellm_provider": "nvidia_nim", + "mode": "rerank", + "output_cost_per_token": 0.0 + }, + "sagemaker/meta-textgeneration-llama-2-13b": { + "input_cost_per_token": 0.0, + "litellm_provider": "sagemaker", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "completion", + "output_cost_per_token": 0.0 + }, + "sagemaker/meta-textgeneration-llama-2-13b-f": { + "input_cost_per_token": 0.0, + "litellm_provider": "sagemaker", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 0.0 + }, + "sagemaker/meta-textgeneration-llama-2-70b": { + "input_cost_per_token": 0.0, + "litellm_provider": "sagemaker", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "completion", + "output_cost_per_token": 0.0 + }, + "sagemaker/meta-textgeneration-llama-2-70b-b-f": { + "input_cost_per_token": 0.0, + "litellm_provider": "sagemaker", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 0.0 + }, + "sagemaker/meta-textgeneration-llama-2-7b": { + "input_cost_per_token": 0.0, + "litellm_provider": "sagemaker", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "completion", + "output_cost_per_token": 0.0 + }, + "sagemaker/meta-textgeneration-llama-2-7b-f": { + "input_cost_per_token": 0.0, + "litellm_provider": "sagemaker", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 0.0 + }, + "sambanova/DeepSeek-R1": { + "input_cost_per_token": 5e-06, + "litellm_provider": "sambanova", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 7e-06, + "source": "https://cloud.sambanova.ai/plans/pricing" + }, + "sambanova/DeepSeek-R1-Distill-Llama-70B": { + "input_cost_per_token": 7e-07, + "litellm_provider": "sambanova", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1.4e-06, + "source": "https://cloud.sambanova.ai/plans/pricing" + }, + "sambanova/DeepSeek-V3-0324": { + "input_cost_per_token": 3e-06, + "litellm_provider": "sambanova", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 4.5e-06, + "source": "https://cloud.sambanova.ai/plans/pricing", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "sambanova/Llama-4-Maverick-17B-128E-Instruct": { + "input_cost_per_token": 6.3e-07, + "litellm_provider": "sambanova", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "metadata": { + "notes": "For vision models, images are converted to 6432 input tokens and are billed at that amount" + }, + "mode": "chat", + "output_cost_per_token": 1.8e-06, + "source": "https://cloud.sambanova.ai/plans/pricing", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "sambanova/Llama-4-Scout-17B-16E-Instruct": { + "input_cost_per_token": 4e-07, + "litellm_provider": "sambanova", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "metadata": { + "notes": "For vision models, images are converted to 6432 input tokens and are billed at that amount" + }, + "mode": "chat", + "output_cost_per_token": 7e-07, + "source": "https://cloud.sambanova.ai/plans/pricing", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "sambanova/Meta-Llama-3.1-405B-Instruct": { + "input_cost_per_token": 5e-06, + "litellm_provider": "sambanova", + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1e-05, + "source": "https://cloud.sambanova.ai/plans/pricing", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "sambanova/Meta-Llama-3.1-8B-Instruct": { + "input_cost_per_token": 1e-07, + "litellm_provider": "sambanova", + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 2e-07, + "source": "https://cloud.sambanova.ai/plans/pricing", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "sambanova/Meta-Llama-3.2-1B-Instruct": { + "input_cost_per_token": 4e-08, + "litellm_provider": "sambanova", + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 8e-08, + "source": "https://cloud.sambanova.ai/plans/pricing" + }, + "sambanova/Meta-Llama-3.2-3B-Instruct": { + "input_cost_per_token": 8e-08, + "litellm_provider": "sambanova", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.6e-07, + "source": "https://cloud.sambanova.ai/plans/pricing" + }, + "sambanova/Meta-Llama-3.3-70B-Instruct": { + "input_cost_per_token": 6e-07, + "litellm_provider": "sambanova", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "source": "https://cloud.sambanova.ai/plans/pricing", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "sambanova/Meta-Llama-Guard-3-8B": { + "input_cost_per_token": 3e-07, + "litellm_provider": "sambanova", + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 3e-07, + "source": "https://cloud.sambanova.ai/plans/pricing" + }, + "sambanova/QwQ-32B": { + "input_cost_per_token": 5e-07, + "litellm_provider": "sambanova", + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1e-06, + "source": "https://cloud.sambanova.ai/plans/pricing" + }, + "sambanova/Qwen2-Audio-7B-Instruct": { + "input_cost_per_token": 5e-07, + "litellm_provider": "sambanova", + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 0.0001, + "source": "https://cloud.sambanova.ai/plans/pricing", + "supports_audio_input": true + }, + "sambanova/Qwen3-32B": { + "input_cost_per_token": 4e-07, + "litellm_provider": "sambanova", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 8e-07, + "source": "https://cloud.sambanova.ai/plans/pricing", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "sambanova/DeepSeek-V3.1": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 3e-06, + "output_cost_per_token": 4.5e-06, + "litellm_provider": "sambanova", + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_reasoning": true, + "source": "https://cloud.sambanova.ai/plans/pricing" + }, + "sambanova/gpt-oss-120b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 3e-06, + "output_cost_per_token": 4.5e-06, + "litellm_provider": "sambanova", + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_reasoning": true, + "source": "https://cloud.sambanova.ai/plans/pricing" + }, + + "snowflake/claude-3-5-sonnet": { + "litellm_provider": "snowflake", + "max_input_tokens": 18000, + "max_output_tokens": 8192, + "max_tokens": 18000, + "mode": "chat", + "supports_computer_use": true + }, + "snowflake/deepseek-r1": { + "litellm_provider": "snowflake", + "max_input_tokens": 32768, + "max_output_tokens": 8192, + "max_tokens": 32768, + "mode": "chat", + "supports_reasoning": true + }, + "snowflake/gemma-7b": { + "litellm_provider": "snowflake", + "max_input_tokens": 8000, + "max_output_tokens": 8192, + "max_tokens": 8000, + "mode": "chat" + }, + "snowflake/jamba-1.5-large": { + "litellm_provider": "snowflake", + "max_input_tokens": 256000, + "max_output_tokens": 8192, + "max_tokens": 256000, + "mode": "chat" + }, + "snowflake/jamba-1.5-mini": { + "litellm_provider": "snowflake", + "max_input_tokens": 256000, + "max_output_tokens": 8192, + "max_tokens": 256000, + "mode": "chat" + }, + "snowflake/jamba-instruct": { + "litellm_provider": "snowflake", + "max_input_tokens": 256000, + "max_output_tokens": 8192, + "max_tokens": 256000, + "mode": "chat" + }, + "snowflake/llama2-70b-chat": { + "litellm_provider": "snowflake", + "max_input_tokens": 4096, + "max_output_tokens": 8192, + "max_tokens": 4096, + "mode": "chat" + }, + "snowflake/llama3-70b": { + "litellm_provider": "snowflake", + "max_input_tokens": 8000, + "max_output_tokens": 8192, + "max_tokens": 8000, + "mode": "chat" + }, + "snowflake/llama3-8b": { + "litellm_provider": "snowflake", + "max_input_tokens": 8000, + "max_output_tokens": 8192, + "max_tokens": 8000, + "mode": "chat" + }, + "snowflake/llama3.1-405b": { + "litellm_provider": "snowflake", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 128000, + "mode": "chat" + }, + "snowflake/llama3.1-70b": { + "litellm_provider": "snowflake", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 128000, + "mode": "chat" + }, + "snowflake/llama3.1-8b": { + "litellm_provider": "snowflake", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 128000, + "mode": "chat" + }, + "snowflake/llama3.2-1b": { + "litellm_provider": "snowflake", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 128000, + "mode": "chat" + }, + "snowflake/llama3.2-3b": { + "litellm_provider": "snowflake", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 128000, + "mode": "chat" + }, + "snowflake/llama3.3-70b": { + "litellm_provider": "snowflake", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 128000, + "mode": "chat" + }, + "snowflake/mistral-7b": { + "litellm_provider": "snowflake", + "max_input_tokens": 32000, + "max_output_tokens": 8192, + "max_tokens": 32000, + "mode": "chat" + }, + "snowflake/mistral-large": { + "litellm_provider": "snowflake", + "max_input_tokens": 32000, + "max_output_tokens": 8192, + "max_tokens": 32000, + "mode": "chat" + }, + "snowflake/mistral-large2": { + "litellm_provider": "snowflake", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 128000, + "mode": "chat" + }, + "snowflake/mixtral-8x7b": { + "litellm_provider": "snowflake", + "max_input_tokens": 32000, + "max_output_tokens": 8192, + "max_tokens": 32000, + "mode": "chat" + }, + "snowflake/reka-core": { + "litellm_provider": "snowflake", + "max_input_tokens": 32000, + "max_output_tokens": 8192, + "max_tokens": 32000, + "mode": "chat" + }, + "snowflake/reka-flash": { + "litellm_provider": "snowflake", + "max_input_tokens": 100000, + "max_output_tokens": 8192, + "max_tokens": 100000, + "mode": "chat" + }, + "snowflake/snowflake-arctic": { + "litellm_provider": "snowflake", + "max_input_tokens": 4096, + "max_output_tokens": 8192, + "max_tokens": 4096, + "mode": "chat" + }, + "snowflake/snowflake-llama-3.1-405b": { + "litellm_provider": "snowflake", + "max_input_tokens": 8000, + "max_output_tokens": 8192, + "max_tokens": 8000, + "mode": "chat" + }, + "snowflake/snowflake-llama-3.3-70b": { + "litellm_provider": "snowflake", + "max_input_tokens": 8000, + "max_output_tokens": 8192, + "max_tokens": 8000, + "mode": "chat" + }, + "stability/sd3": { + "litellm_provider": "stability", + "mode": "image_generation", + "output_cost_per_image": 0.065, + "supported_endpoints": ["/v1/images/generations"] + }, + "stability/sd3-large": { + "litellm_provider": "stability", + "mode": "image_generation", + "output_cost_per_image": 0.065, + "supported_endpoints": ["/v1/images/generations"] + }, + "stability/sd3-large-turbo": { + "litellm_provider": "stability", + "mode": "image_generation", + "output_cost_per_image": 0.04, + "supported_endpoints": ["/v1/images/generations"] + }, + "stability/sd3-medium": { + "litellm_provider": "stability", + "mode": "image_generation", + "output_cost_per_image": 0.035, + "supported_endpoints": ["/v1/images/generations"] + }, + "stability/sd3.5-large": { + "litellm_provider": "stability", + "mode": "image_generation", + "output_cost_per_image": 0.065, + "supported_endpoints": ["/v1/images/generations"] + }, + "stability/sd3.5-large-turbo": { + "litellm_provider": "stability", + "mode": "image_generation", + "output_cost_per_image": 0.04, + "supported_endpoints": ["/v1/images/generations"] + }, + "stability/sd3.5-medium": { + "litellm_provider": "stability", + "mode": "image_generation", + "output_cost_per_image": 0.035, + "supported_endpoints": ["/v1/images/generations"] + }, + "stability/stable-image-ultra": { + "litellm_provider": "stability", + "mode": "image_generation", + "output_cost_per_image": 0.08, + "supported_endpoints": ["/v1/images/generations"] + }, + "stability/stable-image-core": { + "litellm_provider": "stability", + "mode": "image_generation", + "output_cost_per_image": 0.03, + "supported_endpoints": ["/v1/images/generations"] + }, + "stability.sd3-5-large-v1:0": { + "litellm_provider": "bedrock", + "max_input_tokens": 77, + "max_tokens": 77, + "mode": "image_generation", + "output_cost_per_image": 0.08 + }, + "stability.sd3-large-v1:0": { + "litellm_provider": "bedrock", + "max_input_tokens": 77, + "max_tokens": 77, + "mode": "image_generation", + "output_cost_per_image": 0.08 + }, + "stability.stable-image-core-v1:0": { + "litellm_provider": "bedrock", + "max_input_tokens": 77, + "max_tokens": 77, + "mode": "image_generation", + "output_cost_per_image": 0.04 + }, + "stability.stable-image-core-v1:1": { + "litellm_provider": "bedrock", + "max_input_tokens": 77, + "max_tokens": 77, + "mode": "image_generation", + "output_cost_per_image": 0.04 + }, + "stability.stable-image-ultra-v1:0": { + "litellm_provider": "bedrock", + "max_input_tokens": 77, + "max_tokens": 77, + "mode": "image_generation", + "output_cost_per_image": 0.14 + }, + "stability.stable-image-ultra-v1:1": { + "litellm_provider": "bedrock", + "max_input_tokens": 77, + "max_tokens": 77, + "mode": "image_generation", + "output_cost_per_image": 0.14 + }, + "standard/1024-x-1024/dall-e-3": { + "input_cost_per_pixel": 3.81469e-08, + "litellm_provider": "openai", + "mode": "image_generation", + "output_cost_per_pixel": 0.0 + }, + "standard/1024-x-1792/dall-e-3": { + "input_cost_per_pixel": 4.359e-08, + "litellm_provider": "openai", + "mode": "image_generation", + "output_cost_per_pixel": 0.0 + }, + "standard/1792-x-1024/dall-e-3": { + "input_cost_per_pixel": 4.359e-08, + "litellm_provider": "openai", + "mode": "image_generation", + "output_cost_per_pixel": 0.0 + }, + "tavily/search": { + "input_cost_per_query": 0.008, + "litellm_provider": "tavily", + "mode": "search" + }, + "tavily/search-advanced": { + "input_cost_per_query": 0.016, + "litellm_provider": "tavily", + "mode": "search" + }, + "text-bison": { + "input_cost_per_character": 2.5e-07, + "litellm_provider": "vertex_ai-text-models", + "max_input_tokens": 8192, + "max_output_tokens": 2048, + "max_tokens": 2048, + "mode": "completion", + "output_cost_per_character": 5e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "text-bison32k": { + "input_cost_per_character": 2.5e-07, + "input_cost_per_token": 1.25e-07, + "litellm_provider": "vertex_ai-text-models", + "max_input_tokens": 8192, + "max_output_tokens": 1024, + "max_tokens": 1024, + "mode": "completion", + "output_cost_per_character": 5e-07, + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "text-bison32k@002": { + "input_cost_per_character": 2.5e-07, + "input_cost_per_token": 1.25e-07, + "litellm_provider": "vertex_ai-text-models", + "max_input_tokens": 8192, + "max_output_tokens": 1024, + "max_tokens": 1024, + "mode": "completion", + "output_cost_per_character": 5e-07, + "output_cost_per_token": 1.25e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "text-bison@001": { + "input_cost_per_character": 2.5e-07, + "litellm_provider": "vertex_ai-text-models", + "max_input_tokens": 8192, + "max_output_tokens": 1024, + "max_tokens": 1024, + "mode": "completion", + "output_cost_per_character": 5e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "text-bison@002": { + "input_cost_per_character": 2.5e-07, + "litellm_provider": "vertex_ai-text-models", + "max_input_tokens": 8192, + "max_output_tokens": 1024, + "max_tokens": 1024, + "mode": "completion", + "output_cost_per_character": 5e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "text-completion-codestral/codestral-2405": { + "input_cost_per_token": 0.0, + "litellm_provider": "text-completion-codestral", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "completion", + "output_cost_per_token": 0.0, + "source": "https://docs.mistral.ai/capabilities/code_generation/" + }, + "text-completion-codestral/codestral-latest": { + "input_cost_per_token": 0.0, + "litellm_provider": "text-completion-codestral", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "completion", + "output_cost_per_token": 0.0, + "source": "https://docs.mistral.ai/capabilities/code_generation/" + }, + "text-embedding-004": { + "input_cost_per_character": 2.5e-08, + "input_cost_per_token": 1e-07, + "litellm_provider": "vertex_ai-embedding-models", + "max_input_tokens": 2048, + "max_tokens": 2048, + "mode": "embedding", + "output_cost_per_token": 0, + "output_vector_size": 768, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models" + }, + "text-embedding-005": { + "input_cost_per_character": 2.5e-08, + "input_cost_per_token": 1e-07, + "litellm_provider": "vertex_ai-embedding-models", + "max_input_tokens": 2048, + "max_tokens": 2048, + "mode": "embedding", + "output_cost_per_token": 0, + "output_vector_size": 768, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models" + }, + "text-embedding-3-large": { + "input_cost_per_token": 1.3e-07, + "input_cost_per_token_batches": 6.5e-08, + "litellm_provider": "openai", + "max_input_tokens": 8191, + "max_tokens": 8191, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_cost_per_token_batches": 0.0, + "output_vector_size": 3072 + }, + "text-embedding-3-small": { + "input_cost_per_token": 2e-08, + "input_cost_per_token_batches": 1e-08, + "litellm_provider": "openai", + "max_input_tokens": 8191, + "max_tokens": 8191, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_cost_per_token_batches": 0.0, + "output_vector_size": 1536 + }, + "text-embedding-ada-002": { + "input_cost_per_token": 1e-07, + "litellm_provider": "openai", + "max_input_tokens": 8191, + "max_tokens": 8191, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 1536 + }, + "text-embedding-ada-002-v2": { + "input_cost_per_token": 1e-07, + "input_cost_per_token_batches": 5e-08, + "litellm_provider": "openai", + "max_input_tokens": 8191, + "max_tokens": 8191, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_cost_per_token_batches": 0.0 + }, + "text-embedding-large-exp-03-07": { + "input_cost_per_character": 2.5e-08, + "input_cost_per_token": 1e-07, + "litellm_provider": "vertex_ai-embedding-models", + "max_input_tokens": 8192, + "max_tokens": 8192, + "mode": "embedding", + "output_cost_per_token": 0, + "output_vector_size": 3072, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models" + }, + "text-embedding-preview-0409": { + "input_cost_per_token": 6.25e-09, + "input_cost_per_token_batch_requests": 5e-09, + "litellm_provider": "vertex_ai-embedding-models", + "max_input_tokens": 3072, + "max_tokens": 3072, + "mode": "embedding", + "output_cost_per_token": 0, + "output_vector_size": 768, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing" + }, + "text-moderation-007": { + "input_cost_per_token": 0.0, + "litellm_provider": "openai", + "max_input_tokens": 32768, + "max_output_tokens": 0, + "max_tokens": 32768, + "mode": "moderation", + "output_cost_per_token": 0.0 + }, + "text-moderation-latest": { + "input_cost_per_token": 0.0, + "litellm_provider": "openai", + "max_input_tokens": 32768, + "max_output_tokens": 0, + "max_tokens": 32768, + "mode": "moderation", + "output_cost_per_token": 0.0 + }, + "text-moderation-stable": { + "input_cost_per_token": 0.0, + "litellm_provider": "openai", + "max_input_tokens": 32768, + "max_output_tokens": 0, + "max_tokens": 32768, + "mode": "moderation", + "output_cost_per_token": 0.0 + }, + "text-multilingual-embedding-002": { + "input_cost_per_character": 2.5e-08, + "input_cost_per_token": 1e-07, + "litellm_provider": "vertex_ai-embedding-models", + "max_input_tokens": 2048, + "max_tokens": 2048, + "mode": "embedding", + "output_cost_per_token": 0, + "output_vector_size": 768, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models" + }, + "text-multilingual-embedding-preview-0409": { + "input_cost_per_token": 6.25e-09, + "litellm_provider": "vertex_ai-embedding-models", + "max_input_tokens": 3072, + "max_tokens": 3072, + "mode": "embedding", + "output_cost_per_token": 0, + "output_vector_size": 768, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "text-unicorn": { + "input_cost_per_token": 1e-05, + "litellm_provider": "vertex_ai-text-models", + "max_input_tokens": 8192, + "max_output_tokens": 1024, + "max_tokens": 1024, + "mode": "completion", + "output_cost_per_token": 2.8e-05, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "text-unicorn@001": { + "input_cost_per_token": 1e-05, + "litellm_provider": "vertex_ai-text-models", + "max_input_tokens": 8192, + "max_output_tokens": 1024, + "max_tokens": 1024, + "mode": "completion", + "output_cost_per_token": 2.8e-05, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "textembedding-gecko": { + "input_cost_per_character": 2.5e-08, + "input_cost_per_token": 1e-07, + "litellm_provider": "vertex_ai-embedding-models", + "max_input_tokens": 3072, + "max_tokens": 3072, + "mode": "embedding", + "output_cost_per_token": 0, + "output_vector_size": 768, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "textembedding-gecko-multilingual": { + "input_cost_per_character": 2.5e-08, + "input_cost_per_token": 1e-07, + "litellm_provider": "vertex_ai-embedding-models", + "max_input_tokens": 3072, + "max_tokens": 3072, + "mode": "embedding", + "output_cost_per_token": 0, + "output_vector_size": 768, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "textembedding-gecko-multilingual@001": { + "input_cost_per_character": 2.5e-08, + "input_cost_per_token": 1e-07, + "litellm_provider": "vertex_ai-embedding-models", + "max_input_tokens": 3072, + "max_tokens": 3072, + "mode": "embedding", + "output_cost_per_token": 0, + "output_vector_size": 768, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "textembedding-gecko@001": { + "input_cost_per_character": 2.5e-08, + "input_cost_per_token": 1e-07, + "litellm_provider": "vertex_ai-embedding-models", + "max_input_tokens": 3072, + "max_tokens": 3072, + "mode": "embedding", + "output_cost_per_token": 0, + "output_vector_size": 768, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "textembedding-gecko@003": { + "input_cost_per_character": 2.5e-08, + "input_cost_per_token": 1e-07, + "litellm_provider": "vertex_ai-embedding-models", + "max_input_tokens": 3072, + "max_tokens": 3072, + "mode": "embedding", + "output_cost_per_token": 0, + "output_vector_size": 768, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#foundation_models" + }, + "together-ai-21.1b-41b": { + "input_cost_per_token": 8e-07, + "litellm_provider": "together_ai", + "mode": "chat", + "output_cost_per_token": 8e-07 + }, + "together-ai-4.1b-8b": { + "input_cost_per_token": 2e-07, + "litellm_provider": "together_ai", + "mode": "chat", + "output_cost_per_token": 2e-07 + }, + "together-ai-41.1b-80b": { + "input_cost_per_token": 9e-07, + "litellm_provider": "together_ai", + "mode": "chat", + "output_cost_per_token": 9e-07 + }, + "together-ai-8.1b-21b": { + "input_cost_per_token": 3e-07, + "litellm_provider": "together_ai", + "max_tokens": 1000, + "mode": "chat", + "output_cost_per_token": 3e-07 + }, + "together-ai-81.1b-110b": { + "input_cost_per_token": 1.8e-06, + "litellm_provider": "together_ai", + "mode": "chat", + "output_cost_per_token": 1.8e-06 + }, + "together-ai-embedding-151m-to-350m": { + "input_cost_per_token": 1.6e-08, + "litellm_provider": "together_ai", + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "together-ai-embedding-up-to-150m": { + "input_cost_per_token": 8e-09, + "litellm_provider": "together_ai", + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "together_ai/baai/bge-base-en-v1.5": { + "input_cost_per_token": 8e-09, + "litellm_provider": "together_ai", + "max_input_tokens": 512, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 768 + }, + "together_ai/BAAI/bge-base-en-v1.5": { + "input_cost_per_token": 8e-09, + "litellm_provider": "together_ai", + "max_input_tokens": 512, + "mode": "embedding", + "output_cost_per_token": 0.0, + "output_vector_size": 768 + }, + "together-ai-up-to-4b": { + "input_cost_per_token": 1e-07, + "litellm_provider": "together_ai", + "mode": "chat", + "output_cost_per_token": 1e-07 + }, + "together_ai/Qwen/Qwen2.5-72B-Instruct-Turbo": { + "litellm_provider": "together_ai", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "together_ai/Qwen/Qwen2.5-7B-Instruct-Turbo": { + "litellm_provider": "together_ai", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "together_ai/Qwen/Qwen3-235B-A22B-Instruct-2507-tput": { + "input_cost_per_token": 2e-07, + "litellm_provider": "together_ai", + "max_input_tokens": 262000, + "mode": "chat", + "output_cost_per_token": 6e-06, + "source": "https://www.together.ai/models/qwen3-235b-a22b-instruct-2507-fp8", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "together_ai/Qwen/Qwen3-235B-A22B-Thinking-2507": { + "input_cost_per_token": 6.5e-07, + "litellm_provider": "together_ai", + "max_input_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 3e-06, + "source": "https://www.together.ai/models/qwen3-235b-a22b-thinking-2507", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "together_ai/Qwen/Qwen3-235B-A22B-fp8-tput": { + "input_cost_per_token": 2e-07, + "litellm_provider": "together_ai", + "max_input_tokens": 40000, + "mode": "chat", + "output_cost_per_token": 6e-07, + "source": "https://www.together.ai/models/qwen3-235b-a22b-fp8-tput", + "supports_function_calling": false, + "supports_parallel_function_calling": false, + "supports_tool_choice": false + }, + "together_ai/Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8": { + "input_cost_per_token": 2e-06, + "litellm_provider": "together_ai", + "max_input_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 2e-06, + "source": "https://www.together.ai/models/qwen3-coder-480b-a35b-instruct", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "together_ai/deepseek-ai/DeepSeek-R1": { + "input_cost_per_token": 3e-06, + "litellm_provider": "together_ai", + "max_input_tokens": 128000, + "max_output_tokens": 20480, + "max_tokens": 20480, + "mode": "chat", + "output_cost_per_token": 7e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "together_ai/deepseek-ai/DeepSeek-R1-0528-tput": { + "input_cost_per_token": 5.5e-07, + "litellm_provider": "together_ai", + "max_input_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 2.19e-06, + "source": "https://www.together.ai/models/deepseek-r1-0528-throughput", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "together_ai/deepseek-ai/DeepSeek-V3": { + "input_cost_per_token": 1.25e-06, + "litellm_provider": "together_ai", + "max_input_tokens": 65536, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.25e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "together_ai/deepseek-ai/DeepSeek-V3.1": { + "input_cost_per_token": 6e-07, + "litellm_provider": "together_ai", + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.7e-06, + "source": "https://www.together.ai/models/deepseek-v3-1", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "together_ai/meta-llama/Llama-3.2-3B-Instruct-Turbo": { + "litellm_provider": "together_ai", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "together_ai/meta-llama/Llama-3.3-70B-Instruct-Turbo": { + "input_cost_per_token": 8.8e-07, + "litellm_provider": "together_ai", + "mode": "chat", + "output_cost_per_token": 8.8e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "together_ai/meta-llama/Llama-3.3-70B-Instruct-Turbo-Free": { + "input_cost_per_token": 0, + "litellm_provider": "together_ai", + "mode": "chat", + "output_cost_per_token": 0, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "together_ai/meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8": { + "input_cost_per_token": 2.7e-07, + "litellm_provider": "together_ai", + "mode": "chat", + "output_cost_per_token": 8.5e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "together_ai/meta-llama/Llama-4-Scout-17B-16E-Instruct": { + "input_cost_per_token": 1.8e-07, + "litellm_provider": "together_ai", + "mode": "chat", + "output_cost_per_token": 5.9e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "together_ai/meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo": { + "input_cost_per_token": 3.5e-06, + "litellm_provider": "together_ai", + "mode": "chat", + "output_cost_per_token": 3.5e-06, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "together_ai/meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo": { + "input_cost_per_token": 8.8e-07, + "litellm_provider": "together_ai", + "mode": "chat", + "output_cost_per_token": 8.8e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "together_ai/meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo": { + "input_cost_per_token": 1.8e-07, + "litellm_provider": "together_ai", + "mode": "chat", + "output_cost_per_token": 1.8e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "together_ai/mistralai/Mistral-7B-Instruct-v0.1": { + "litellm_provider": "together_ai", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "together_ai/mistralai/Mistral-Small-24B-Instruct-2501": { + "litellm_provider": "together_ai", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "together_ai/mistralai/Mixtral-8x7B-Instruct-v0.1": { + "input_cost_per_token": 6e-07, + "litellm_provider": "together_ai", + "mode": "chat", + "output_cost_per_token": 6e-07, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "together_ai/moonshotai/Kimi-K2-Instruct": { + "input_cost_per_token": 1e-06, + "litellm_provider": "together_ai", + "mode": "chat", + "output_cost_per_token": 3e-06, + "source": "https://www.together.ai/models/kimi-k2-instruct", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "together_ai/openai/gpt-oss-120b": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "together_ai", + "max_input_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 6e-07, + "source": "https://www.together.ai/models/gpt-oss-120b", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "together_ai/openai/gpt-oss-20b": { + "input_cost_per_token": 5e-08, + "litellm_provider": "together_ai", + "max_input_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 2e-07, + "source": "https://www.together.ai/models/gpt-oss-20b", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "together_ai/togethercomputer/CodeLlama-34b-Instruct": { + "litellm_provider": "together_ai", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "together_ai/zai-org/GLM-4.5-Air-FP8": { + "input_cost_per_token": 2e-07, + "litellm_provider": "together_ai", + "max_input_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.1e-06, + "source": "https://www.together.ai/models/glm-4-5-air", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "together_ai/zai-org/GLM-4.6": { + "input_cost_per_token": 0.6e-06, + "litellm_provider": "together_ai", + "max_input_tokens": 200000, + "max_output_tokens": 200000, + "max_tokens": 200000, + "mode": "chat", + "output_cost_per_token": 2.2e-06, + "source": "https://www.together.ai/models/glm-4-6", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "together_ai/moonshotai/Kimi-K2-Instruct-0905": { + "input_cost_per_token": 1e-06, + "litellm_provider": "together_ai", + "max_input_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 3e-06, + "source": "https://www.together.ai/models/kimi-k2-0905", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "together_ai/Qwen/Qwen3-Next-80B-A3B-Instruct": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "together_ai", + "max_input_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "source": "https://www.together.ai/models/qwen3-next-80b-a3b-instruct", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "together_ai/Qwen/Qwen3-Next-80B-A3B-Thinking": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "together_ai", + "max_input_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "source": "https://www.together.ai/models/qwen3-next-80b-a3b-thinking", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "tts-1": { + "input_cost_per_character": 1.5e-05, + "litellm_provider": "openai", + "mode": "audio_speech", + "supported_endpoints": [ + "/v1/audio/speech" + ] + }, + "tts-1-hd": { + "input_cost_per_character": 3e-05, + "litellm_provider": "openai", + "mode": "audio_speech", + "supported_endpoints": [ + "/v1/audio/speech" + ] + }, + "us.amazon.nova-lite-v1:0": { + "input_cost_per_token": 6e-08, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 300000, + "max_output_tokens": 10000, + "max_tokens": 10000, + "mode": "chat", + "output_cost_per_token": 2.4e-07, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_vision": true + }, + "us.amazon.nova-micro-v1:0": { + "input_cost_per_token": 3.5e-08, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 10000, + "max_tokens": 10000, + "mode": "chat", + "output_cost_per_token": 1.4e-07, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true + }, + "us.amazon.nova-premier-v1:0": { + "input_cost_per_token": 2.5e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 1000000, + "max_output_tokens": 10000, + "max_tokens": 10000, + "mode": "chat", + "output_cost_per_token": 1.25e-05, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": false, + "supports_response_schema": true, + "supports_vision": true + }, + "us.amazon.nova-pro-v1:0": { + "input_cost_per_token": 8e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 300000, + "max_output_tokens": 10000, + "max_tokens": 10000, + "mode": "chat", + "output_cost_per_token": 3.2e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_vision": true + }, + "us.anthropic.claude-3-5-haiku-20241022-v1:0": { + "cache_creation_input_token_cost": 1e-06, + "cache_read_input_token_cost": 8e-08, + "input_cost_per_token": 8e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 4e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "us.anthropic.claude-haiku-4-5-20251001-v1:0": { + "cache_creation_input_token_cost": 1.375e-06, + "cache_read_input_token_cost": 1.1e-07, + "input_cost_per_token": 1.1e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 5.5e-06, + "source": "https://aws.amazon.com/about-aws/whats-new/2025/10/claude-4-5-haiku-anthropic-amazon-bedrock", + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 346 + }, + "us.anthropic.claude-3-5-sonnet-20240620-v1:0": { + "input_cost_per_token": 3e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "us.anthropic.claude-3-5-sonnet-20241022-v2:0": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "us.anthropic.claude-3-7-sonnet-20250219-v1:0": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "us.anthropic.claude-3-haiku-20240307-v1:0": { + "input_cost_per_token": 2.5e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.25e-06, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "us.anthropic.claude-3-opus-20240229-v1:0": { + "input_cost_per_token": 1.5e-05, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "us.anthropic.claude-3-sonnet-20240229-v1:0": { + "input_cost_per_token": 3e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "us.anthropic.claude-opus-4-1-20250805-v1:0": { + "cache_creation_input_token_cost": 1.875e-05, + "cache_read_input_token_cost": 1.5e-06, + "input_cost_per_token": 1.5e-05, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "us.anthropic.claude-sonnet-4-5-20250929-v1:0": { + "cache_creation_input_token_cost": 4.125e-06, + "cache_read_input_token_cost": 3.3e-07, + "input_cost_per_token": 3.3e-06, + "input_cost_per_token_above_200k_tokens": 6.6e-06, + "output_cost_per_token_above_200k_tokens": 2.475e-05, + "cache_creation_input_token_cost_above_200k_tokens": 8.25e-06, + "cache_read_input_token_cost_above_200k_tokens": 6.6e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.65e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 346 + }, + "au.anthropic.claude-haiku-4-5-20251001-v1:0": { + "cache_creation_input_token_cost": 1.375e-06, + "cache_read_input_token_cost": 1.1e-07, + "input_cost_per_token": 1.1e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 5.5e-06, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 346 + }, + "us.anthropic.claude-opus-4-20250514-v1:0": { + "cache_creation_input_token_cost": 1.875e-05, + "cache_read_input_token_cost": 1.5e-06, + "input_cost_per_token": 1.5e-05, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "us.anthropic.claude-opus-4-5-20251101-v1:0": { + "cache_creation_input_token_cost": 6.25e-06, + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 5e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "global.anthropic.claude-opus-4-5-20251101-v1:0": { + "cache_creation_input_token_cost": 6.25e-06, + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 5e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "eu.anthropic.claude-opus-4-5-20251101-v1:0": { + "cache_creation_input_token_cost": 6.25e-06, + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 5e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "us.anthropic.claude-sonnet-4-20250514-v1:0": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "us.deepseek.r1-v1:0": { + "input_cost_per_token": 1.35e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 5.4e-06, + "supports_function_calling": false, + "supports_reasoning": true, + "supports_tool_choice": false + }, + "us.meta.llama3-1-405b-instruct-v1:0": { + "input_cost_per_token": 5.32e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.6e-05, + "supports_function_calling": true, + "supports_tool_choice": false + }, + "us.meta.llama3-1-70b-instruct-v1:0": { + "input_cost_per_token": 9.9e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 128000, + "max_output_tokens": 2048, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 9.9e-07, + "supports_function_calling": true, + "supports_tool_choice": false + }, + "us.meta.llama3-1-8b-instruct-v1:0": { + "input_cost_per_token": 2.2e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 128000, + "max_output_tokens": 2048, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 2.2e-07, + "supports_function_calling": true, + "supports_tool_choice": false + }, + "us.meta.llama3-2-11b-instruct-v1:0": { + "input_cost_per_token": 3.5e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 3.5e-07, + "supports_function_calling": true, + "supports_tool_choice": false, + "supports_vision": true + }, + "us.meta.llama3-2-1b-instruct-v1:0": { + "input_cost_per_token": 1e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-07, + "supports_function_calling": true, + "supports_tool_choice": false + }, + "us.meta.llama3-2-3b-instruct-v1:0": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "bedrock", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.5e-07, + "supports_function_calling": true, + "supports_tool_choice": false + }, + "us.meta.llama3-2-90b-instruct-v1:0": { + "input_cost_per_token": 2e-06, + "litellm_provider": "bedrock", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 2e-06, + "supports_function_calling": true, + "supports_tool_choice": false, + "supports_vision": true + }, + "us.meta.llama3-3-70b-instruct-v1:0": { + "input_cost_per_token": 7.2e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 7.2e-07, + "supports_function_calling": true, + "supports_tool_choice": false + }, + "us.meta.llama4-maverick-17b-instruct-v1:0": { + "input_cost_per_token": 2.4e-07, + "input_cost_per_token_batches": 1.2e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 9.7e-07, + "output_cost_per_token_batches": 4.85e-07, + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "code" + ], + "supports_function_calling": true, + "supports_tool_choice": false + }, + "us.meta.llama4-scout-17b-instruct-v1:0": { + "input_cost_per_token": 1.7e-07, + "input_cost_per_token_batches": 8.5e-08, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 6.6e-07, + "output_cost_per_token_batches": 3.3e-07, + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "code" + ], + "supports_function_calling": true, + "supports_tool_choice": false + }, + "us.mistral.pixtral-large-2502-v1:0": { + "input_cost_per_token": 2e-06, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 6e-06, + "supports_function_calling": true, + "supports_tool_choice": false + }, + "v0/v0-1.0-md": { + "input_cost_per_token": 3e-06, + "litellm_provider": "v0", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "v0/v0-1.5-lg": { + "input_cost_per_token": 1.5e-05, + "litellm_provider": "v0", + "max_input_tokens": 512000, + "max_output_tokens": 512000, + "max_tokens": 512000, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "v0/v0-1.5-md": { + "input_cost_per_token": 3e-06, + "litellm_provider": "v0", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "vercel_ai_gateway/alibaba/qwen-3-14b": { + "input_cost_per_token": 8e-08, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 40960, + "max_output_tokens": 16384, + "max_tokens": 40960, + "mode": "chat", + "output_cost_per_token": 2.4e-07 + }, + "vercel_ai_gateway/alibaba/qwen-3-235b": { + "input_cost_per_token": 2e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 40960, + "max_output_tokens": 16384, + "max_tokens": 40960, + "mode": "chat", + "output_cost_per_token": 6e-07 + }, + "vercel_ai_gateway/alibaba/qwen-3-30b": { + "input_cost_per_token": 1e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 40960, + "max_output_tokens": 16384, + "max_tokens": 40960, + "mode": "chat", + "output_cost_per_token": 3e-07 + }, + "vercel_ai_gateway/alibaba/qwen-3-32b": { + "input_cost_per_token": 1e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 40960, + "max_output_tokens": 16384, + "max_tokens": 40960, + "mode": "chat", + "output_cost_per_token": 3e-07 + }, + "vercel_ai_gateway/alibaba/qwen3-coder": { + "input_cost_per_token": 4e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 262144, + "max_output_tokens": 66536, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 1.6e-06 + }, + "vercel_ai_gateway/amazon/nova-lite": { + "input_cost_per_token": 6e-08, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 300000, + "max_output_tokens": 8192, + "max_tokens": 300000, + "mode": "chat", + "output_cost_per_token": 2.4e-07 + }, + "vercel_ai_gateway/amazon/nova-micro": { + "input_cost_per_token": 3.5e-08, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.4e-07 + }, + "vercel_ai_gateway/amazon/nova-pro": { + "input_cost_per_token": 8e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 300000, + "max_output_tokens": 8192, + "max_tokens": 300000, + "mode": "chat", + "output_cost_per_token": 3.2e-06 + }, + "vercel_ai_gateway/amazon/titan-embed-text-v2": { + "input_cost_per_token": 2e-08, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 0, + "max_output_tokens": 0, + "max_tokens": 0, + "mode": "chat", + "output_cost_per_token": 0.0 + }, + "vercel_ai_gateway/anthropic/claude-3-haiku": { + "cache_creation_input_token_cost": 3e-07, + "cache_read_input_token_cost": 3e-08, + "input_cost_per_token": 2.5e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 200000, + "mode": "chat", + "output_cost_per_token": 1.25e-06 + }, + "vercel_ai_gateway/anthropic/claude-3-opus": { + "cache_creation_input_token_cost": 1.875e-05, + "cache_read_input_token_cost": 1.5e-06, + "input_cost_per_token": 1.5e-05, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 200000, + "mode": "chat", + "output_cost_per_token": 7.5e-05 + }, + "vercel_ai_gateway/anthropic/claude-3.5-haiku": { + "cache_creation_input_token_cost": 1e-06, + "cache_read_input_token_cost": 8e-08, + "input_cost_per_token": 8e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 200000, + "mode": "chat", + "output_cost_per_token": 4e-06 + }, + "vercel_ai_gateway/anthropic/claude-3.5-sonnet": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 200000, + "mode": "chat", + "output_cost_per_token": 1.5e-05 + }, + "vercel_ai_gateway/anthropic/claude-3.7-sonnet": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 200000, + "mode": "chat", + "output_cost_per_token": 1.5e-05 + }, + "vercel_ai_gateway/anthropic/claude-4-opus": { + "cache_creation_input_token_cost": 1.875e-05, + "cache_read_input_token_cost": 1.5e-06, + "input_cost_per_token": 1.5e-05, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 200000, + "mode": "chat", + "output_cost_per_token": 7.5e-05 + }, + "vercel_ai_gateway/anthropic/claude-4-sonnet": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 200000, + "mode": "chat", + "output_cost_per_token": 1.5e-05 + }, + "vercel_ai_gateway/cohere/command-a": { + "input_cost_per_token": 2.5e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 256000, + "max_output_tokens": 8000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 1e-05 + }, + "vercel_ai_gateway/cohere/command-r": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 6e-07 + }, + "vercel_ai_gateway/cohere/command-r-plus": { + "input_cost_per_token": 2.5e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05 + }, + "vercel_ai_gateway/cohere/embed-v4.0": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 0, + "max_output_tokens": 0, + "max_tokens": 0, + "mode": "chat", + "output_cost_per_token": 0.0 + }, + "vercel_ai_gateway/deepseek/deepseek-r1": { + "input_cost_per_token": 5.5e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 2.19e-06 + }, + "vercel_ai_gateway/deepseek/deepseek-r1-distill-llama-70b": { + "input_cost_per_token": 7.5e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 9.9e-07 + }, + "vercel_ai_gateway/deepseek/deepseek-v3": { + "input_cost_per_token": 9e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 9e-07 + }, + "vercel_ai_gateway/google/gemini-2.0-flash": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_tokens": 1048576, + "mode": "chat", + "output_cost_per_token": 6e-07 + }, + "vercel_ai_gateway/google/gemini-2.0-flash-lite": { + "input_cost_per_token": 7.5e-08, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 1048576, + "max_output_tokens": 8192, + "max_tokens": 1048576, + "mode": "chat", + "output_cost_per_token": 3e-07 + }, + "vercel_ai_gateway/google/gemini-2.5-flash": { + "input_cost_per_token": 3e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 1000000, + "max_output_tokens": 65536, + "max_tokens": 1000000, + "mode": "chat", + "output_cost_per_token": 2.5e-06 + }, + "vercel_ai_gateway/google/gemini-2.5-pro": { + "input_cost_per_token": 2.5e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 1048576, + "max_output_tokens": 65536, + "max_tokens": 1048576, + "mode": "chat", + "output_cost_per_token": 1e-05 + }, + "vercel_ai_gateway/google/gemini-embedding-001": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 0, + "max_output_tokens": 0, + "max_tokens": 0, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "vercel_ai_gateway/google/gemma-2-9b": { + "input_cost_per_token": 2e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2e-07 + }, + "vercel_ai_gateway/google/text-embedding-005": { + "input_cost_per_token": 2.5e-08, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 0, + "max_output_tokens": 0, + "max_tokens": 0, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "vercel_ai_gateway/google/text-multilingual-embedding-002": { + "input_cost_per_token": 2.5e-08, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 0, + "max_output_tokens": 0, + "max_tokens": 0, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "vercel_ai_gateway/inception/mercury-coder-small": { + "input_cost_per_token": 2.5e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 32000, + "max_output_tokens": 16384, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 1e-06 + }, + "vercel_ai_gateway/meta/llama-3-70b": { + "input_cost_per_token": 5.9e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 7.9e-07 + }, + "vercel_ai_gateway/meta/llama-3-8b": { + "input_cost_per_token": 5e-08, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 8e-08 + }, + "vercel_ai_gateway/meta/llama-3.1-70b": { + "input_cost_per_token": 7.2e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 7.2e-07 + }, + "vercel_ai_gateway/meta/llama-3.1-8b": { + "input_cost_per_token": 5e-08, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 131000, + "max_output_tokens": 131072, + "max_tokens": 131000, + "mode": "chat", + "output_cost_per_token": 8e-08 + }, + "vercel_ai_gateway/meta/llama-3.2-11b": { + "input_cost_per_token": 1.6e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.6e-07 + }, + "vercel_ai_gateway/meta/llama-3.2-1b": { + "input_cost_per_token": 1e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-07 + }, + "vercel_ai_gateway/meta/llama-3.2-3b": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.5e-07 + }, + "vercel_ai_gateway/meta/llama-3.2-90b": { + "input_cost_per_token": 7.2e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 7.2e-07 + }, + "vercel_ai_gateway/meta/llama-3.3-70b": { + "input_cost_per_token": 7.2e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 128000, + "max_output_tokens": 8192, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 7.2e-07 + }, + "vercel_ai_gateway/meta/llama-4-maverick": { + "input_cost_per_token": 2e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 131072, + "max_output_tokens": 8192, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 6e-07 + }, + "vercel_ai_gateway/meta/llama-4-scout": { + "input_cost_per_token": 1e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 131072, + "max_output_tokens": 8192, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 3e-07 + }, + "vercel_ai_gateway/mistral/codestral": { + "input_cost_per_token": 3e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 256000, + "max_output_tokens": 4000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 9e-07 + }, + "vercel_ai_gateway/mistral/codestral-embed": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 0, + "max_output_tokens": 0, + "max_tokens": 0, + "mode": "chat", + "output_cost_per_token": 0.0 + }, + "vercel_ai_gateway/mistral/devstral-small": { + "input_cost_per_token": 7e-08, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 2.8e-07 + }, + "vercel_ai_gateway/mistral/magistral-medium": { + "input_cost_per_token": 2e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 128000, + "max_output_tokens": 64000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 5e-06 + }, + "vercel_ai_gateway/mistral/magistral-small": { + "input_cost_per_token": 5e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 128000, + "max_output_tokens": 64000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.5e-06 + }, + "vercel_ai_gateway/mistral/ministral-3b": { + "input_cost_per_token": 4e-08, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 128000, + "max_output_tokens": 4000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 4e-08 + }, + "vercel_ai_gateway/mistral/ministral-8b": { + "input_cost_per_token": 1e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 128000, + "max_output_tokens": 4000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-07 + }, + "vercel_ai_gateway/mistral/mistral-embed": { + "input_cost_per_token": 1e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 0, + "max_output_tokens": 0, + "max_tokens": 0, + "mode": "chat", + "output_cost_per_token": 0.0 + }, + "vercel_ai_gateway/mistral/mistral-large": { + "input_cost_per_token": 2e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 32000, + "max_output_tokens": 4000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 6e-06 + }, + "vercel_ai_gateway/mistral/mistral-saba-24b": { + "input_cost_per_token": 7.9e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 7.9e-07 + }, + "vercel_ai_gateway/mistral/mistral-small": { + "input_cost_per_token": 1e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 32000, + "max_output_tokens": 4000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 3e-07 + }, + "vercel_ai_gateway/mistral/mixtral-8x22b-instruct": { + "input_cost_per_token": 1.2e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 65536, + "max_output_tokens": 2048, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 1.2e-06 + }, + "vercel_ai_gateway/mistral/pixtral-12b": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 128000, + "max_output_tokens": 4000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.5e-07 + }, + "vercel_ai_gateway/mistral/pixtral-large": { + "input_cost_per_token": 2e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 128000, + "max_output_tokens": 4000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 6e-06 + }, + "vercel_ai_gateway/moonshotai/kimi-k2": { + "input_cost_per_token": 5.5e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 131072, + "max_output_tokens": 16384, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2.2e-06 + }, + "vercel_ai_gateway/morph/morph-v3-fast": { + "input_cost_per_token": 8e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 32768, + "max_output_tokens": 16384, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 1.2e-06 + }, + "vercel_ai_gateway/morph/morph-v3-large": { + "input_cost_per_token": 9e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 32768, + "max_output_tokens": 16384, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 1.9e-06 + }, + "vercel_ai_gateway/openai/gpt-3.5-turbo": { + "input_cost_per_token": 5e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 16385, + "max_output_tokens": 4096, + "max_tokens": 16385, + "mode": "chat", + "output_cost_per_token": 1.5e-06 + }, + "vercel_ai_gateway/openai/gpt-3.5-turbo-instruct": { + "input_cost_per_token": 1.5e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 8192, + "max_output_tokens": 4096, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2e-06 + }, + "vercel_ai_gateway/openai/gpt-4-turbo": { + "input_cost_per_token": 1e-05, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 128000, + "max_output_tokens": 4096, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 3e-05 + }, + "vercel_ai_gateway/openai/gpt-4.1": { + "cache_creation_input_token_cost": 0.0, + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 2e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 1047576, + "mode": "chat", + "output_cost_per_token": 8e-06 + }, + "vercel_ai_gateway/openai/gpt-4.1-mini": { + "cache_creation_input_token_cost": 0.0, + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 4e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 1047576, + "mode": "chat", + "output_cost_per_token": 1.6e-06 + }, + "vercel_ai_gateway/openai/gpt-4.1-nano": { + "cache_creation_input_token_cost": 0.0, + "cache_read_input_token_cost": 2.5e-08, + "input_cost_per_token": 1e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 1047576, + "max_output_tokens": 32768, + "max_tokens": 1047576, + "mode": "chat", + "output_cost_per_token": 4e-07 + }, + "vercel_ai_gateway/openai/gpt-4o": { + "cache_creation_input_token_cost": 0.0, + "cache_read_input_token_cost": 1.25e-06, + "input_cost_per_token": 2.5e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05 + }, + "vercel_ai_gateway/openai/gpt-4o-mini": { + "cache_creation_input_token_cost": 0.0, + "cache_read_input_token_cost": 7.5e-08, + "input_cost_per_token": 1.5e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 128000, + "max_output_tokens": 16384, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 6e-07 + }, + "vercel_ai_gateway/openai/o1": { + "cache_creation_input_token_cost": 0.0, + "cache_read_input_token_cost": 7.5e-06, + "input_cost_per_token": 1.5e-05, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 200000, + "mode": "chat", + "output_cost_per_token": 6e-05 + }, + "vercel_ai_gateway/openai/o3": { + "cache_creation_input_token_cost": 0.0, + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 2e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 200000, + "mode": "chat", + "output_cost_per_token": 8e-06 + }, + "vercel_ai_gateway/openai/o3-mini": { + "cache_creation_input_token_cost": 0.0, + "cache_read_input_token_cost": 5.5e-07, + "input_cost_per_token": 1.1e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 200000, + "mode": "chat", + "output_cost_per_token": 4.4e-06 + }, + "vercel_ai_gateway/openai/o4-mini": { + "cache_creation_input_token_cost": 0.0, + "cache_read_input_token_cost": 2.75e-07, + "input_cost_per_token": 1.1e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 200000, + "max_output_tokens": 100000, + "max_tokens": 200000, + "mode": "chat", + "output_cost_per_token": 4.4e-06 + }, + "vercel_ai_gateway/openai/text-embedding-3-large": { + "input_cost_per_token": 1.3e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 0, + "max_output_tokens": 0, + "max_tokens": 0, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "vercel_ai_gateway/openai/text-embedding-3-small": { + "input_cost_per_token": 2e-08, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 0, + "max_output_tokens": 0, + "max_tokens": 0, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "vercel_ai_gateway/openai/text-embedding-ada-002": { + "input_cost_per_token": 1e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 0, + "max_output_tokens": 0, + "max_tokens": 0, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "vercel_ai_gateway/perplexity/sonar": { + "input_cost_per_token": 1e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 127000, + "max_output_tokens": 8000, + "max_tokens": 127000, + "mode": "chat", + "output_cost_per_token": 1e-06 + }, + "vercel_ai_gateway/perplexity/sonar-pro": { + "input_cost_per_token": 3e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 200000, + "max_output_tokens": 8000, + "max_tokens": 200000, + "mode": "chat", + "output_cost_per_token": 1.5e-05 + }, + "vercel_ai_gateway/perplexity/sonar-reasoning": { + "input_cost_per_token": 1e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 127000, + "max_output_tokens": 8000, + "max_tokens": 127000, + "mode": "chat", + "output_cost_per_token": 5e-06 + }, + "vercel_ai_gateway/perplexity/sonar-reasoning-pro": { + "input_cost_per_token": 2e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 127000, + "max_output_tokens": 8000, + "max_tokens": 127000, + "mode": "chat", + "output_cost_per_token": 8e-06 + }, + "vercel_ai_gateway/vercel/v0-1.0-md": { + "input_cost_per_token": 3e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.5e-05 + }, + "vercel_ai_gateway/vercel/v0-1.5-md": { + "input_cost_per_token": 3e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 128000, + "max_output_tokens": 32768, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.5e-05 + }, + "vercel_ai_gateway/xai/grok-2": { + "input_cost_per_token": 2e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 131072, + "max_output_tokens": 4000, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1e-05 + }, + "vercel_ai_gateway/xai/grok-2-vision": { + "input_cost_per_token": 2e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 1e-05 + }, + "vercel_ai_gateway/xai/grok-3": { + "input_cost_per_token": 3e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1.5e-05 + }, + "vercel_ai_gateway/xai/grok-3-fast": { + "input_cost_per_token": 5e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2.5e-05 + }, + "vercel_ai_gateway/xai/grok-3-mini": { + "input_cost_per_token": 3e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 5e-07 + }, + "vercel_ai_gateway/xai/grok-3-mini-fast": { + "input_cost_per_token": 6e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 4e-06 + }, + "vercel_ai_gateway/xai/grok-4": { + "input_cost_per_token": 3e-06, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 1.5e-05 + }, + "vercel_ai_gateway/zai/glm-4.5": { + "input_cost_per_token": 6e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2.2e-06 + }, + "vercel_ai_gateway/zai/glm-4.5-air": { + "input_cost_per_token": 2e-07, + "litellm_provider": "vercel_ai_gateway", + "max_input_tokens": 128000, + "max_output_tokens": 96000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.1e-06 + }, + "vercel_ai_gateway/zai/glm-4.6": { + "litellm_provider": "vercel_ai_gateway", + "cache_read_input_token_cost": 1.1e-07, + "input_cost_per_token": 4.5e-07, + "max_input_tokens": 200000, + "max_output_tokens": 200000, + "max_tokens": 200000, + "mode": "chat", + "output_cost_per_token": 1.8e-06, + "source": "https://vercel.com/ai-gateway/models/glm-4.6", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/chirp": { + "input_cost_per_character": 30e-06, + "litellm_provider": "vertex_ai", + "mode": "audio_speech", + "source": "https://cloud.google.com/text-to-speech/pricing", + "supported_endpoints": [ + "/v1/audio/speech" + ] + }, + "vertex_ai/claude-3-5-haiku": { + "input_cost_per_token": 1e-06, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 5e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_tool_choice": true + }, + "vertex_ai/claude-3-5-haiku@20241022": { + "input_cost_per_token": 1e-06, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 5e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_tool_choice": true + }, + "vertex_ai/claude-haiku-4-5@20251001": { + "cache_creation_input_token_cost": 1.25e-06, + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 1e-06, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 5e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/claude/haiku-4-5", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "vertex_ai/claude-3-5-sonnet": { + "input_cost_per_token": 3e-06, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "vertex_ai/claude-3-5-sonnet-v2": { + "input_cost_per_token": 3e-06, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "vertex_ai/claude-3-5-sonnet-v2@20241022": { + "input_cost_per_token": 3e-06, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "vertex_ai/claude-3-5-sonnet@20240620": { + "input_cost_per_token": 3e-06, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "vertex_ai/claude-3-7-sonnet@20250219": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "deprecation_date": "2025-06-01", + "input_cost_per_token": 3e-06, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "vertex_ai/claude-3-haiku": { + "input_cost_per_token": 2.5e-07, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.25e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "vertex_ai/claude-3-haiku@20240307": { + "input_cost_per_token": 2.5e-07, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.25e-06, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "vertex_ai/claude-3-opus": { + "input_cost_per_token": 1.5e-05, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "vertex_ai/claude-3-opus@20240229": { + "input_cost_per_token": 1.5e-05, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "vertex_ai/claude-3-sonnet": { + "input_cost_per_token": 3e-06, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "vertex_ai/claude-3-sonnet@20240229": { + "input_cost_per_token": 3e-06, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 200000, + "max_output_tokens": 4096, + "max_tokens": 4096, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "vertex_ai/claude-opus-4": { + "cache_creation_input_token_cost": 1.875e-05, + "cache_read_input_token_cost": 1.5e-06, + "input_cost_per_token": 1.5e-05, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "vertex_ai/claude-opus-4-1": { + "cache_creation_input_token_cost": 1.875e-05, + "cache_read_input_token_cost": 1.5e-06, + "input_cost_per_token": 1.5e-05, + "input_cost_per_token_batches": 7.5e-06, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "output_cost_per_token_batches": 3.75e-05, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "vertex_ai/claude-opus-4-1@20250805": { + "cache_creation_input_token_cost": 1.875e-05, + "cache_read_input_token_cost": 1.5e-06, + "input_cost_per_token": 1.5e-05, + "input_cost_per_token_batches": 7.5e-06, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "output_cost_per_token_batches": 3.75e-05, + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "vertex_ai/claude-opus-4-5": { + "cache_creation_input_token_cost": 6.25e-06, + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 5e-06, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "vertex_ai/claude-opus-4-5@20251101": { + "cache_creation_input_token_cost": 6.25e-06, + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 5e-06, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 2.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "vertex_ai/claude-sonnet-4-5": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "input_cost_per_token_batches": 1.5e-06, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "output_cost_per_token_batches": 7.5e-06, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "vertex_ai/claude-sonnet-4-5@20250929": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "input_cost_per_token_batches": 1.5e-06, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 200000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "output_cost_per_token_batches": 7.5e-06, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "vertex_ai/claude-opus-4@20250514": { + "cache_creation_input_token_cost": 1.875e-05, + "cache_read_input_token_cost": 1.5e-06, + "input_cost_per_token": 1.5e-05, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 200000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 7.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "vertex_ai/claude-sonnet-4": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "vertex_ai/claude-sonnet-4@20250514": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "litellm_provider": "vertex_ai-anthropic_models", + "max_input_tokens": 1000000, + "max_output_tokens": 64000, + "max_tokens": 64000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "search_context_cost_per_query": { + "search_context_size_high": 0.01, + "search_context_size_low": 0.01, + "search_context_size_medium": 0.01 + }, + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "vertex_ai/mistralai/codestral-2@001": { + "input_cost_per_token": 3e-07, + "litellm_provider": "vertex_ai-mistral_models", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 9e-07, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/codestral-2": { + "input_cost_per_token": 3e-07, + "litellm_provider": "vertex_ai-mistral_models", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 9e-07, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/codestral-2@001": { + "input_cost_per_token": 3e-07, + "litellm_provider": "vertex_ai-mistral_models", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 9e-07, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/mistralai/codestral-2": { + "input_cost_per_token": 3e-07, + "litellm_provider": "vertex_ai-mistral_models", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 9e-07, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/codestral-2501": { + "input_cost_per_token": 2e-07, + "litellm_provider": "vertex_ai-mistral_models", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 6e-07, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/codestral@2405": { + "input_cost_per_token": 2e-07, + "litellm_provider": "vertex_ai-mistral_models", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 6e-07, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/codestral@latest": { + "input_cost_per_token": 2e-07, + "litellm_provider": "vertex_ai-mistral_models", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 6e-07, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/deepseek-ai/deepseek-v3.1-maas": { + "input_cost_per_token": 1.35e-06, + "litellm_provider": "vertex_ai-deepseek_models", + "max_input_tokens": 163840, + "max_output_tokens": 32768, + "max_tokens": 163840, + "mode": "chat", + "output_cost_per_token": 5.4e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#partner-models", + "supported_regions": [ + "us-west2" + ], + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "vertex_ai/deepseek-ai/deepseek-v3.2-maas": { + "input_cost_per_token": 5.6e-07, + "input_cost_per_token_batches": 2.8e-07, + "litellm_provider": "vertex_ai-deepseek_models", + "max_input_tokens": 163840, + "max_output_tokens": 32768, + "max_tokens": 163840, + "mode": "chat", + "output_cost_per_token": 1.68e-06, + "output_cost_per_token_batches": 8.4e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#partner-models", + "supported_regions": [ + "us-west2" + ], + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "vertex_ai/deepseek-ai/deepseek-r1-0528-maas": { + "input_cost_per_token": 1.35e-06, + "litellm_provider": "vertex_ai-deepseek_models", + "max_input_tokens": 65336, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 5.4e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#partner-models", + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "vertex_ai/gemini-2.5-flash-image": { + "cache_read_input_token_cost": 3e-08, + "input_cost_per_audio_token": 1e-06, + "input_cost_per_token": 3e-07, + "litellm_provider": "vertex_ai-language-models", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "max_pdf_size_mb": 30, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "image_generation", + "output_cost_per_image": 0.039, + "output_cost_per_reasoning_token": 2.5e-06, + "output_cost_per_token": 2.5e-06, + "rpm": 100000, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/image-generation#edit-an-image", + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text", + "image" + ], + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_url_context": true, + "supports_vision": true, + "supports_web_search": false, + "tpm": 8000000 + }, + "vertex_ai/gemini-3-pro-image-preview": { + "input_cost_per_image": 0.0011, + "input_cost_per_token": 2e-06, + "input_cost_per_token_batches": 1e-06, + "litellm_provider": "vertex_ai-language-models", + "max_input_tokens": 65536, + "max_output_tokens": 32768, + "max_tokens": 65536, + "mode": "image_generation", + "output_cost_per_image": 0.134, + "output_cost_per_image_token": 1.2e-04, + "output_cost_per_token": 1.2e-05, + "output_cost_per_token_batches": 6e-06, + "source": "https://docs.cloud.google.com/vertex-ai/generative-ai/docs/models/gemini/3-pro-image" + }, + "vertex_ai/imagegeneration@006": { + "litellm_provider": "vertex_ai-image-models", + "mode": "image_generation", + "output_cost_per_image": 0.02, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing" + }, + "vertex_ai/imagen-3.0-fast-generate-001": { + "litellm_provider": "vertex_ai-image-models", + "mode": "image_generation", + "output_cost_per_image": 0.02, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing" + }, + "vertex_ai/imagen-3.0-generate-001": { + "litellm_provider": "vertex_ai-image-models", + "mode": "image_generation", + "output_cost_per_image": 0.04, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing" + }, + "vertex_ai/imagen-3.0-generate-002": { + "litellm_provider": "vertex_ai-image-models", + "mode": "image_generation", + "output_cost_per_image": 0.04, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing" + }, + "vertex_ai/imagen-3.0-capability-001": { + "litellm_provider": "vertex_ai-image-models", + "mode": "image_generation", + "output_cost_per_image": 0.04, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/image/edit-insert-objects" + }, + "vertex_ai/imagen-4.0-fast-generate-001": { + "litellm_provider": "vertex_ai-image-models", + "mode": "image_generation", + "output_cost_per_image": 0.02, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing" + }, + "vertex_ai/imagen-4.0-generate-001": { + "litellm_provider": "vertex_ai-image-models", + "mode": "image_generation", + "output_cost_per_image": 0.04, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing" + }, + "vertex_ai/imagen-4.0-ultra-generate-001": { + "litellm_provider": "vertex_ai-image-models", + "mode": "image_generation", + "output_cost_per_image": 0.06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing" + }, + "vertex_ai/jamba-1.5": { + "input_cost_per_token": 2e-07, + "litellm_provider": "vertex_ai-ai21_models", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supports_tool_choice": true + }, + "vertex_ai/jamba-1.5-large": { + "input_cost_per_token": 2e-06, + "litellm_provider": "vertex_ai-ai21_models", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 8e-06, + "supports_tool_choice": true + }, + "vertex_ai/jamba-1.5-large@001": { + "input_cost_per_token": 2e-06, + "litellm_provider": "vertex_ai-ai21_models", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 8e-06, + "supports_tool_choice": true + }, + "vertex_ai/jamba-1.5-mini": { + "input_cost_per_token": 2e-07, + "litellm_provider": "vertex_ai-ai21_models", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supports_tool_choice": true + }, + "vertex_ai/jamba-1.5-mini@001": { + "input_cost_per_token": 2e-07, + "litellm_provider": "vertex_ai-ai21_models", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 4e-07, + "supports_tool_choice": true + }, + "vertex_ai/meta/llama-3.1-405b-instruct-maas": { + "input_cost_per_token": 5e-06, + "litellm_provider": "vertex_ai-llama_models", + "max_input_tokens": 128000, + "max_output_tokens": 2048, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.6e-05, + "source": "https://console.cloud.google.com/vertex-ai/publishers/meta/model-garden/llama-3.2-90b-vision-instruct-maas", + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "vertex_ai/meta/llama-3.1-70b-instruct-maas": { + "input_cost_per_token": 0.0, + "litellm_provider": "vertex_ai-llama_models", + "max_input_tokens": 128000, + "max_output_tokens": 2048, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 0.0, + "source": "https://console.cloud.google.com/vertex-ai/publishers/meta/model-garden/llama-3.2-90b-vision-instruct-maas", + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "vertex_ai/meta/llama-3.1-8b-instruct-maas": { + "input_cost_per_token": 0.0, + "litellm_provider": "vertex_ai-llama_models", + "max_input_tokens": 128000, + "max_output_tokens": 2048, + "max_tokens": 128000, + "metadata": { + "notes": "VertexAI states that The Llama 3.1 API service for llama-3.1-70b-instruct-maas and llama-3.1-8b-instruct-maas are in public preview and at no cost." + }, + "mode": "chat", + "output_cost_per_token": 0.0, + "source": "https://console.cloud.google.com/vertex-ai/publishers/meta/model-garden/llama-3.2-90b-vision-instruct-maas", + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "vertex_ai/meta/llama-3.2-90b-vision-instruct-maas": { + "input_cost_per_token": 0.0, + "litellm_provider": "vertex_ai-llama_models", + "max_input_tokens": 128000, + "max_output_tokens": 2048, + "max_tokens": 128000, + "metadata": { + "notes": "VertexAI states that The Llama 3.2 API service is at no cost during public preview, and will be priced as per dollar-per-1M-tokens at GA." + }, + "mode": "chat", + "output_cost_per_token": 0.0, + "source": "https://console.cloud.google.com/vertex-ai/publishers/meta/model-garden/llama-3.2-90b-vision-instruct-maas", + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "vertex_ai/meta/llama-4-maverick-17b-128e-instruct-maas": { + "input_cost_per_token": 3.5e-07, + "litellm_provider": "vertex_ai-llama_models", + "max_input_tokens": 1000000, + "max_output_tokens": 1000000, + "max_tokens": 1000000, + "mode": "chat", + "output_cost_per_token": 1.15e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#partner-models", + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "code" + ], + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/meta/llama-4-maverick-17b-16e-instruct-maas": { + "input_cost_per_token": 3.5e-07, + "litellm_provider": "vertex_ai-llama_models", + "max_input_tokens": 1000000, + "max_output_tokens": 1000000, + "max_tokens": 1000000, + "mode": "chat", + "output_cost_per_token": 1.15e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#partner-models", + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "code" + ], + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/meta/llama-4-scout-17b-128e-instruct-maas": { + "input_cost_per_token": 2.5e-07, + "litellm_provider": "vertex_ai-llama_models", + "max_input_tokens": 10000000, + "max_output_tokens": 10000000, + "max_tokens": 10000000, + "mode": "chat", + "output_cost_per_token": 7e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#partner-models", + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "code" + ], + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/meta/llama-4-scout-17b-16e-instruct-maas": { + "input_cost_per_token": 2.5e-07, + "litellm_provider": "vertex_ai-llama_models", + "max_input_tokens": 10000000, + "max_output_tokens": 10000000, + "max_tokens": 10000000, + "mode": "chat", + "output_cost_per_token": 7e-07, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#partner-models", + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text", + "code" + ], + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/meta/llama3-405b-instruct-maas": { + "input_cost_per_token": 0.0, + "litellm_provider": "vertex_ai-llama_models", + "max_input_tokens": 32000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 0.0, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#partner-models", + "supports_tool_choice": true + }, + "vertex_ai/meta/llama3-70b-instruct-maas": { + "input_cost_per_token": 0.0, + "litellm_provider": "vertex_ai-llama_models", + "max_input_tokens": 32000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 0.0, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#partner-models", + "supports_tool_choice": true + }, + "vertex_ai/meta/llama3-8b-instruct-maas": { + "input_cost_per_token": 0.0, + "litellm_provider": "vertex_ai-llama_models", + "max_input_tokens": 32000, + "max_output_tokens": 32000, + "max_tokens": 32000, + "mode": "chat", + "output_cost_per_token": 0.0, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#partner-models", + "supports_tool_choice": true + }, + "vertex_ai/minimaxai/minimax-m2-maas": { + "input_cost_per_token": 3e-07, + "litellm_provider": "vertex_ai-minimax_models", + "max_input_tokens": 196608, + "max_output_tokens": 196608, + "max_tokens": 196608, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#partner-models", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/moonshotai/kimi-k2-thinking-maas": { + "input_cost_per_token": 6e-07, + "litellm_provider": "vertex_ai-moonshot_models", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 2.5e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing#partner-models", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "vertex_ai/mistral-medium-3": { + "input_cost_per_token": 4e-07, + "litellm_provider": "vertex_ai-mistral_models", + "max_input_tokens": 128000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2e-06, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/mistral-medium-3@001": { + "input_cost_per_token": 4e-07, + "litellm_provider": "vertex_ai-mistral_models", + "max_input_tokens": 128000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2e-06, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/mistralai/mistral-medium-3": { + "input_cost_per_token": 4e-07, + "litellm_provider": "vertex_ai-mistral_models", + "max_input_tokens": 128000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2e-06, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/mistralai/mistral-medium-3@001": { + "input_cost_per_token": 4e-07, + "litellm_provider": "vertex_ai-mistral_models", + "max_input_tokens": 128000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 2e-06, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/mistral-large-2411": { + "input_cost_per_token": 2e-06, + "litellm_provider": "vertex_ai-mistral_models", + "max_input_tokens": 128000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 6e-06, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/mistral-large@2407": { + "input_cost_per_token": 2e-06, + "litellm_provider": "vertex_ai-mistral_models", + "max_input_tokens": 128000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 6e-06, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/mistral-large@2411-001": { + "input_cost_per_token": 2e-06, + "litellm_provider": "vertex_ai-mistral_models", + "max_input_tokens": 128000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 6e-06, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/mistral-large@latest": { + "input_cost_per_token": 2e-06, + "litellm_provider": "vertex_ai-mistral_models", + "max_input_tokens": 128000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 6e-06, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/mistral-nemo@2407": { + "input_cost_per_token": 3e-06, + "litellm_provider": "vertex_ai-mistral_models", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 3e-06, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/mistral-nemo@latest": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "vertex_ai-mistral_models", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.5e-07, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/mistral-small-2503": { + "input_cost_per_token": 1e-06, + "litellm_provider": "vertex_ai-mistral_models", + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 3e-06, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "vertex_ai/mistral-small-2503@001": { + "input_cost_per_token": 1e-06, + "litellm_provider": "vertex_ai-mistral_models", + "max_input_tokens": 32000, + "max_output_tokens": 8191, + "max_tokens": 8191, + "mode": "chat", + "output_cost_per_token": 3e-06, + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/mistral-ocr-2505": { + "litellm_provider": "vertex_ai", + "mode": "ocr", + "ocr_cost_per_page": 5e-4, + "supported_endpoints": [ + "/v1/ocr" + ], + "source": "https://cloud.google.com/generative-ai-app-builder/pricing" + }, + "vertex_ai/openai/gpt-oss-120b-maas": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "vertex_ai-openai_models", + "max_input_tokens": 131072, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 6e-07, + "source": "https://console.cloud.google.com/vertex-ai/publishers/openai/model-garden/gpt-oss-120b-maas", + "supports_reasoning": true + }, + "vertex_ai/openai/gpt-oss-20b-maas": { + "input_cost_per_token": 7.5e-08, + "litellm_provider": "vertex_ai-openai_models", + "max_input_tokens": 131072, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 3e-07, + "source": "https://console.cloud.google.com/vertex-ai/publishers/openai/model-garden/gpt-oss-120b-maas", + "supports_reasoning": true + }, + "vertex_ai/qwen/qwen3-235b-a22b-instruct-2507-maas": { + "input_cost_per_token": 2.5e-07, + "litellm_provider": "vertex_ai-qwen_models", + "max_input_tokens": 262144, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 1e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/qwen/qwen3-coder-480b-a35b-instruct-maas": { + "input_cost_per_token": 1e-06, + "litellm_provider": "vertex_ai-qwen_models", + "max_input_tokens": 262144, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 4e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/qwen/qwen3-next-80b-a3b-instruct-maas": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "vertex_ai-qwen_models", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/qwen/qwen3-next-80b-a3b-thinking-maas": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "vertex_ai-qwen_models", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "source": "https://cloud.google.com/vertex-ai/generative-ai/pricing", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "vertex_ai/veo-2.0-generate-001": { + "litellm_provider": "vertex_ai-video-models", + "max_input_tokens": 1024, + "max_tokens": 1024, + "mode": "video_generation", + "output_cost_per_second": 0.35, + "source": "https://ai.google.dev/gemini-api/docs/video", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "video" + ] + }, + "vertex_ai/veo-3.0-fast-generate-preview": { + "litellm_provider": "vertex_ai-video-models", + "max_input_tokens": 1024, + "max_tokens": 1024, + "mode": "video_generation", + "output_cost_per_second": 0.15, + "source": "https://ai.google.dev/gemini-api/docs/video", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "video" + ] + }, + "vertex_ai/veo-3.0-generate-preview": { + "litellm_provider": "vertex_ai-video-models", + "max_input_tokens": 1024, + "max_tokens": 1024, + "mode": "video_generation", + "output_cost_per_second": 0.4, + "source": "https://ai.google.dev/gemini-api/docs/video", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "video" + ] + }, + "vertex_ai/veo-3.0-fast-generate-001": { + "litellm_provider": "vertex_ai-video-models", + "max_input_tokens": 1024, + "max_tokens": 1024, + "mode": "video_generation", + "output_cost_per_second": 0.15, + "source": "https://ai.google.dev/gemini-api/docs/video", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "video" + ] + }, + "vertex_ai/veo-3.0-generate-001": { + "litellm_provider": "vertex_ai-video-models", + "max_input_tokens": 1024, + "max_tokens": 1024, + "mode": "video_generation", + "output_cost_per_second": 0.4, + "source": "https://ai.google.dev/gemini-api/docs/video", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "video" + ] + }, + "vertex_ai/veo-3.1-generate-preview": { + "litellm_provider": "vertex_ai-video-models", + "max_input_tokens": 1024, + "max_tokens": 1024, + "mode": "video_generation", + "output_cost_per_second": 0.4, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/veo", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "video" + ] + }, + "vertex_ai/veo-3.1-fast-generate-preview": { + "litellm_provider": "vertex_ai-video-models", + "max_input_tokens": 1024, + "max_tokens": 1024, + "mode": "video_generation", + "output_cost_per_second": 0.15, + "source": "https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/veo", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "video" + ] + }, + "voyage/rerank-2": { + "input_cost_per_token": 5e-08, + "litellm_provider": "voyage", + "max_input_tokens": 16000, + "max_output_tokens": 16000, + "max_query_tokens": 16000, + "max_tokens": 16000, + "mode": "rerank", + "output_cost_per_token": 0.0 + }, + "voyage/rerank-2-lite": { + "input_cost_per_token": 2e-08, + "litellm_provider": "voyage", + "max_input_tokens": 8000, + "max_output_tokens": 8000, + "max_query_tokens": 8000, + "max_tokens": 8000, + "mode": "rerank", + "output_cost_per_token": 0.0 + }, + "voyage/rerank-2.5": { + "input_cost_per_token": 5e-08, + "litellm_provider": "voyage", + "max_input_tokens": 32000, + "max_output_tokens": 32000, + "max_query_tokens": 32000, + "max_tokens": 32000, + "mode": "rerank", + "output_cost_per_token": 0.0 + }, + "voyage/rerank-2.5-lite": { + "input_cost_per_token": 2e-08, + "litellm_provider": "voyage", + "max_input_tokens": 32000, + "max_output_tokens": 32000, + "max_query_tokens": 32000, + "max_tokens": 32000, + "mode": "rerank", + "output_cost_per_token": 0.0 + }, + "voyage/voyage-2": { + "input_cost_per_token": 1e-07, + "litellm_provider": "voyage", + "max_input_tokens": 4000, + "max_tokens": 4000, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "voyage/voyage-3": { + "input_cost_per_token": 6e-08, + "litellm_provider": "voyage", + "max_input_tokens": 32000, + "max_tokens": 32000, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "voyage/voyage-3-large": { + "input_cost_per_token": 1.8e-07, + "litellm_provider": "voyage", + "max_input_tokens": 32000, + "max_tokens": 32000, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "voyage/voyage-3-lite": { + "input_cost_per_token": 2e-08, + "litellm_provider": "voyage", + "max_input_tokens": 32000, + "max_tokens": 32000, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "voyage/voyage-3.5": { + "input_cost_per_token": 6e-08, + "litellm_provider": "voyage", + "max_input_tokens": 32000, + "max_tokens": 32000, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "voyage/voyage-3.5-lite": { + "input_cost_per_token": 2e-08, + "litellm_provider": "voyage", + "max_input_tokens": 32000, + "max_tokens": 32000, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "voyage/voyage-code-2": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "voyage", + "max_input_tokens": 16000, + "max_tokens": 16000, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "voyage/voyage-code-3": { + "input_cost_per_token": 1.8e-07, + "litellm_provider": "voyage", + "max_input_tokens": 32000, + "max_tokens": 32000, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "voyage/voyage-context-3": { + "input_cost_per_token": 1.8e-07, + "litellm_provider": "voyage", + "max_input_tokens": 120000, + "max_tokens": 120000, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "voyage/voyage-finance-2": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "voyage", + "max_input_tokens": 32000, + "max_tokens": 32000, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "voyage/voyage-large-2": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "voyage", + "max_input_tokens": 16000, + "max_tokens": 16000, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "voyage/voyage-law-2": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "voyage", + "max_input_tokens": 16000, + "max_tokens": 16000, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "voyage/voyage-lite-01": { + "input_cost_per_token": 1e-07, + "litellm_provider": "voyage", + "max_input_tokens": 4096, + "max_tokens": 4096, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "voyage/voyage-lite-02-instruct": { + "input_cost_per_token": 1e-07, + "litellm_provider": "voyage", + "max_input_tokens": 4000, + "max_tokens": 4000, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "voyage/voyage-multimodal-3": { + "input_cost_per_token": 1.2e-07, + "litellm_provider": "voyage", + "max_input_tokens": 32000, + "max_tokens": 32000, + "mode": "embedding", + "output_cost_per_token": 0.0 + }, + "wandb/openai/gpt-oss-120b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 0.015, + "output_cost_per_token": 0.06, + "litellm_provider": "wandb", + "mode": "chat" + }, + "wandb/openai/gpt-oss-20b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 0.005, + "output_cost_per_token": 0.02, + "litellm_provider": "wandb", + "mode": "chat" + }, + "wandb/zai-org/GLM-4.5": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 0.055, + "output_cost_per_token": 0.2, + "litellm_provider": "wandb", + "mode": "chat" + }, + "wandb/Qwen/Qwen3-235B-A22B-Instruct-2507": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 0.01, + "output_cost_per_token": 0.01, + "litellm_provider": "wandb", + "mode": "chat" + }, + "wandb/Qwen/Qwen3-Coder-480B-A35B-Instruct": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 0.1, + "output_cost_per_token": 0.15, + "litellm_provider": "wandb", + "mode": "chat" + }, + "wandb/Qwen/Qwen3-235B-A22B-Thinking-2507": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 0.01, + "output_cost_per_token": 0.01, + "litellm_provider": "wandb", + "mode": "chat" + }, + "wandb/moonshotai/Kimi-K2-Instruct": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "input_cost_per_token": 6e-07, + "output_cost_per_token": 2.5e-06, + "litellm_provider": "wandb", + "mode": "chat" + }, + "wandb/meta-llama/Llama-3.1-8B-Instruct": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "input_cost_per_token": 0.022, + "output_cost_per_token": 0.022, + "litellm_provider": "wandb", + "mode": "chat" + }, + "wandb/deepseek-ai/DeepSeek-V3.1": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "input_cost_per_token": 0.055, + "output_cost_per_token": 0.165, + "litellm_provider": "wandb", + "mode": "chat" + }, + "wandb/deepseek-ai/DeepSeek-R1-0528": { + "max_tokens": 161000, + "max_input_tokens": 161000, + "max_output_tokens": 161000, + "input_cost_per_token": 0.135, + "output_cost_per_token": 0.54, + "litellm_provider": "wandb", + "mode": "chat" + }, + "wandb/deepseek-ai/DeepSeek-V3-0324": { + "max_tokens": 161000, + "max_input_tokens": 161000, + "max_output_tokens": 161000, + "input_cost_per_token": 0.114, + "output_cost_per_token": 0.275, + "litellm_provider": "wandb", + "mode": "chat" + }, + "wandb/meta-llama/Llama-3.3-70B-Instruct": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "input_cost_per_token": 0.071, + "output_cost_per_token": 0.071, + "litellm_provider": "wandb", + "mode": "chat" + }, + "wandb/meta-llama/Llama-4-Scout-17B-16E-Instruct": { + "max_tokens": 64000, + "max_input_tokens": 64000, + "max_output_tokens": 64000, + "input_cost_per_token": 0.017, + "output_cost_per_token": 0.066, + "litellm_provider": "wandb", + "mode": "chat" + }, + "wandb/microsoft/Phi-4-mini-instruct": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "input_cost_per_token": 0.008, + "output_cost_per_token": 0.035, + "litellm_provider": "wandb", + "mode": "chat" + }, + "watsonx/ibm/granite-3-8b-instruct": { + "input_cost_per_token": 0.2e-06, + "litellm_provider": "watsonx", + "max_input_tokens": 8192, + "max_output_tokens": 1024, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 0.2e-06, + "supports_audio_input": false, + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": false, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "watsonx/mistralai/mistral-large": { + "input_cost_per_token": 3e-06, + "litellm_provider": "watsonx", + "max_input_tokens": 131072, + "max_output_tokens": 16384, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 10e-06, + "supports_audio_input": false, + "supports_audio_output": false, + "supports_function_calling": true, + "supports_parallel_function_calling": false, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "watsonx/bigscience/mt0-xxl-13b": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 0.0005, + "output_cost_per_token": 0.002, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": false, + "supports_parallel_function_calling": false, + "supports_vision": false + }, + "watsonx/core42/jais-13b-chat": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 0.0005, + "output_cost_per_token": 0.002, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": false, + "supports_parallel_function_calling": false, + "supports_vision": false + }, + "watsonx/google/flan-t5-xl-3b": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 0.6e-06, + "output_cost_per_token": 0.6e-06, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": false, + "supports_parallel_function_calling": false, + "supports_vision": false + }, + "watsonx/ibm/granite-13b-chat-v2": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 0.6e-06, + "output_cost_per_token": 0.6e-06, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": false, + "supports_parallel_function_calling": false, + "supports_vision": false + }, + "watsonx/ibm/granite-13b-instruct-v2": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 0.6e-06, + "output_cost_per_token": 0.6e-06, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": false, + "supports_parallel_function_calling": false, + "supports_vision": false + }, + "watsonx/ibm/granite-3-3-8b-instruct": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 0.2e-06, + "output_cost_per_token": 0.2e-06, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_vision": false + }, + "watsonx/ibm/granite-4-h-small": { + "max_tokens": 20480, + "max_input_tokens": 20480, + "max_output_tokens": 20480, + "input_cost_per_token": 0.06e-06, + "output_cost_per_token": 0.25e-06, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_vision": false + }, + "watsonx/ibm/granite-guardian-3-2-2b": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 0.1e-06, + "output_cost_per_token": 0.1e-06, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": false, + "supports_parallel_function_calling": false, + "supports_vision": false + }, + "watsonx/ibm/granite-guardian-3-3-8b": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 0.2e-06, + "output_cost_per_token": 0.2e-06, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": false, + "supports_parallel_function_calling": false, + "supports_vision": false + }, + "watsonx/ibm/granite-ttm-1024-96-r2": { + "max_tokens": 512, + "max_input_tokens": 512, + "max_output_tokens": 512, + "input_cost_per_token": 0.38e-06, + "output_cost_per_token": 0.38e-06, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": false, + "supports_parallel_function_calling": false, + "supports_vision": false + }, + "watsonx/ibm/granite-ttm-1536-96-r2": { + "max_tokens": 512, + "max_input_tokens": 512, + "max_output_tokens": 512, + "input_cost_per_token": 0.38e-06, + "output_cost_per_token": 0.38e-06, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": false, + "supports_parallel_function_calling": false, + "supports_vision": false + }, + "watsonx/ibm/granite-ttm-512-96-r2": { + "max_tokens": 512, + "max_input_tokens": 512, + "max_output_tokens": 512, + "input_cost_per_token": 0.38e-06, + "output_cost_per_token": 0.38e-06, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": false, + "supports_parallel_function_calling": false, + "supports_vision": false + }, + "watsonx/ibm/granite-vision-3-2-2b": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 0.1e-06, + "output_cost_per_token": 0.1e-06, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": false, + "supports_parallel_function_calling": false, + "supports_vision": true + }, + "watsonx/meta-llama/llama-3-2-11b-vision-instruct": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "input_cost_per_token": 0.35e-06, + "output_cost_per_token": 0.35e-06, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_vision": true + }, + "watsonx/meta-llama/llama-3-2-1b-instruct": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "input_cost_per_token": 0.1e-06, + "output_cost_per_token": 0.1e-06, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_vision": false + }, + "watsonx/meta-llama/llama-3-2-3b-instruct": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "input_cost_per_token": 0.15e-06, + "output_cost_per_token": 0.15e-06, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_vision": false + }, + "watsonx/meta-llama/llama-3-2-90b-vision-instruct": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "input_cost_per_token": 2e-06, + "output_cost_per_token": 2e-06, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_vision": true + }, + "watsonx/meta-llama/llama-3-3-70b-instruct": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "input_cost_per_token": 0.71e-06, + "output_cost_per_token": 0.71e-06, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_vision": false + }, + "watsonx/meta-llama/llama-4-maverick-17b": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "input_cost_per_token": 0.35e-06, + "output_cost_per_token": 1.4e-06, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_vision": false + }, + "watsonx/meta-llama/llama-guard-3-11b-vision": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "input_cost_per_token": 0.35e-06, + "output_cost_per_token": 0.35e-06, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": false, + "supports_parallel_function_calling": false, + "supports_vision": true + }, + "watsonx/mistralai/mistral-medium-2505": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "input_cost_per_token": 3e-06, + "output_cost_per_token": 10e-06, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_vision": false + }, + "watsonx/mistralai/mistral-small-2503": { + "max_tokens": 32000, + "max_input_tokens": 32000, + "max_output_tokens": 32000, + "input_cost_per_token": 0.1e-06, + "output_cost_per_token": 0.3e-06, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_vision": false + }, + "watsonx/mistralai/mistral-small-3-1-24b-instruct-2503": { + "max_tokens": 32000, + "max_input_tokens": 32000, + "max_output_tokens": 32000, + "input_cost_per_token": 0.1e-06, + "output_cost_per_token": 0.3e-06, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_vision": false + }, + "watsonx/mistralai/pixtral-12b-2409": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "input_cost_per_token": 0.35e-06, + "output_cost_per_token": 0.35e-06, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": false, + "supports_parallel_function_calling": false, + "supports_vision": true + }, + "watsonx/openai/gpt-oss-120b": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 0.15e-06, + "output_cost_per_token": 0.6e-06, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": false, + "supports_parallel_function_calling": false, + "supports_vision": false + }, + "watsonx/sdaia/allam-1-13b-instruct": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 1.8e-06, + "output_cost_per_token": 1.8e-06, + "litellm_provider": "watsonx", + "mode": "chat", + "supports_function_calling": false, + "supports_parallel_function_calling": false, + "supports_vision": false + }, + "watsonx/whisper-large-v3-turbo": { + "input_cost_per_second": 0.0001, + "output_cost_per_second": 0.0001, + "litellm_provider": "watsonx", + "mode": "audio_transcription", + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "whisper-1": { + "input_cost_per_second": 0.0001, + "litellm_provider": "openai", + "mode": "audio_transcription", + "output_cost_per_second": 0.0001, + "supported_endpoints": [ + "/v1/audio/transcriptions" + ] + }, + "xai/grok-2": { + "input_cost_per_token": 2e-06, + "litellm_provider": "xai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "xai/grok-2-1212": { + "input_cost_per_token": 2e-06, + "litellm_provider": "xai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "xai/grok-2-latest": { + "input_cost_per_token": 2e-06, + "litellm_provider": "xai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "xai/grok-2-vision": { + "input_cost_per_image": 2e-06, + "input_cost_per_token": 2e-06, + "litellm_provider": "xai", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "xai/grok-2-vision-1212": { + "input_cost_per_image": 2e-06, + "input_cost_per_token": 2e-06, + "litellm_provider": "xai", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "xai/grok-2-vision-latest": { + "input_cost_per_image": 2e-06, + "input_cost_per_token": 2e-06, + "litellm_provider": "xai", + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 1e-05, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "xai/grok-3": { + "input_cost_per_token": 3e-06, + "litellm_provider": "xai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "source": "https://x.ai/api#pricing", + "supports_function_calling": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_web_search": true + }, + "xai/grok-3-beta": { + "input_cost_per_token": 3e-06, + "litellm_provider": "xai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "source": "https://x.ai/api#pricing", + "supports_function_calling": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_web_search": true + }, + "xai/grok-3-fast-beta": { + "input_cost_per_token": 5e-06, + "litellm_provider": "xai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2.5e-05, + "source": "https://x.ai/api#pricing", + "supports_function_calling": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_web_search": true + }, + "xai/grok-3-fast-latest": { + "input_cost_per_token": 5e-06, + "litellm_provider": "xai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2.5e-05, + "source": "https://x.ai/api#pricing", + "supports_function_calling": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_web_search": true + }, + "xai/grok-3-latest": { + "input_cost_per_token": 3e-06, + "litellm_provider": "xai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "source": "https://x.ai/api#pricing", + "supports_function_calling": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_web_search": true + }, + "xai/grok-3-mini": { + "input_cost_per_token": 3e-07, + "litellm_provider": "xai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 5e-07, + "source": "https://x.ai/api#pricing", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_web_search": true + }, + "xai/grok-3-mini-beta": { + "input_cost_per_token": 3e-07, + "litellm_provider": "xai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 5e-07, + "source": "https://x.ai/api#pricing", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_web_search": true + }, + "xai/grok-3-mini-fast": { + "input_cost_per_token": 6e-07, + "litellm_provider": "xai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 4e-06, + "source": "https://x.ai/api#pricing", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_web_search": true + }, + "xai/grok-3-mini-fast-beta": { + "input_cost_per_token": 6e-07, + "litellm_provider": "xai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 4e-06, + "source": "https://x.ai/api#pricing", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_web_search": true + }, + "xai/grok-3-mini-fast-latest": { + "input_cost_per_token": 6e-07, + "litellm_provider": "xai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 4e-06, + "source": "https://x.ai/api#pricing", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_web_search": true + }, + "xai/grok-3-mini-latest": { + "input_cost_per_token": 3e-07, + "litellm_provider": "xai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 5e-07, + "source": "https://x.ai/api#pricing", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_web_search": true + }, + "xai/grok-4": { + "input_cost_per_token": 3e-06, + "litellm_provider": "xai", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "source": "https://docs.x.ai/docs/models", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "xai/grok-4-fast-reasoning": { + "litellm_provider": "xai", + "max_input_tokens": 2e6, + "max_output_tokens": 2e6, + "max_tokens": 2e6, + "mode": "chat", + "input_cost_per_token": 0.2e-06, + "input_cost_per_token_above_128k_tokens": 0.4e-06, + "output_cost_per_token": 0.5e-06, + "output_cost_per_token_above_128k_tokens": 1e-06, + "cache_read_input_token_cost": 0.05e-06, + "source": "https://docs.x.ai/docs/models", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "xai/grok-4-fast-non-reasoning": { + "litellm_provider": "xai", + "max_input_tokens": 2e6, + "max_output_tokens": 2e6, + "cache_read_input_token_cost": 0.05e-06, + "max_tokens": 2e6, + "mode": "chat", + "input_cost_per_token": 0.2e-06, + "input_cost_per_token_above_128k_tokens": 0.4e-06, + "output_cost_per_token": 0.5e-06, + "output_cost_per_token_above_128k_tokens": 1e-06, + "source": "https://docs.x.ai/docs/models", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "xai/grok-4-0709": { + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_128k_tokens": 6e-06, + "litellm_provider": "xai", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "output_cost_per_token_above_128k_tokens": 30e-06, + "source": "https://docs.x.ai/docs/models", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "xai/grok-4-latest": { + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_128k_tokens": 6e-06, + "litellm_provider": "xai", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "output_cost_per_token_above_128k_tokens": 30e-06, + "source": "https://docs.x.ai/docs/models", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_web_search": true + }, + "xai/grok-4-1-fast": { + "cache_read_input_token_cost": 0.05e-06, + "input_cost_per_token": 0.2e-06, + "input_cost_per_token_above_128k_tokens": 0.4e-06, + "litellm_provider": "xai", + "max_input_tokens": 2e6, + "max_output_tokens": 2e6, + "max_tokens": 2e6, + "mode": "chat", + "output_cost_per_token": 0.5e-06, + "output_cost_per_token_above_128k_tokens": 1e-06, + "source": "https://docs.x.ai/docs/models/grok-4-1-fast-reasoning", + "supports_audio_input": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "xai/grok-4-1-fast-reasoning": { + "cache_read_input_token_cost": 0.05e-06, + "input_cost_per_token": 0.2e-06, + "input_cost_per_token_above_128k_tokens": 0.4e-06, + "litellm_provider": "xai", + "max_input_tokens": 2e6, + "max_output_tokens": 2e6, + "max_tokens": 2e6, + "mode": "chat", + "output_cost_per_token": 0.5e-06, + "output_cost_per_token_above_128k_tokens": 1e-06, + "source": "https://docs.x.ai/docs/models/grok-4-1-fast-reasoning", + "supports_audio_input": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "xai/grok-4-1-fast-reasoning-latest": { + "cache_read_input_token_cost": 0.05e-06, + "input_cost_per_token": 0.2e-06, + "input_cost_per_token_above_128k_tokens": 0.4e-06, + "litellm_provider": "xai", + "max_input_tokens": 2e6, + "max_output_tokens": 2e6, + "max_tokens": 2e6, + "mode": "chat", + "output_cost_per_token": 0.5e-06, + "output_cost_per_token_above_128k_tokens": 1e-06, + "source": "https://docs.x.ai/docs/models/grok-4-1-fast-reasoning", + "supports_audio_input": true, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "xai/grok-4-1-fast-non-reasoning": { + "cache_read_input_token_cost": 0.05e-06, + "input_cost_per_token": 0.2e-06, + "input_cost_per_token_above_128k_tokens": 0.4e-06, + "litellm_provider": "xai", + "max_input_tokens": 2e6, + "max_output_tokens": 2e6, + "max_tokens": 2e6, + "mode": "chat", + "output_cost_per_token": 0.5e-06, + "output_cost_per_token_above_128k_tokens": 1e-06, + "source": "https://docs.x.ai/docs/models/grok-4-1-fast-non-reasoning", + "supports_audio_input": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "xai/grok-4-1-fast-non-reasoning-latest": { + "cache_read_input_token_cost": 0.05e-06, + "input_cost_per_token": 0.2e-06, + "input_cost_per_token_above_128k_tokens": 0.4e-06, + "litellm_provider": "xai", + "max_input_tokens": 2e6, + "max_output_tokens": 2e6, + "max_tokens": 2e6, + "mode": "chat", + "output_cost_per_token": 0.5e-06, + "output_cost_per_token_above_128k_tokens": 1e-06, + "source": "https://docs.x.ai/docs/models/grok-4-1-fast-non-reasoning", + "supports_audio_input": true, + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "xai/grok-beta": { + "input_cost_per_token": 5e-06, + "litellm_provider": "xai", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "xai/grok-code-fast": { + "cache_read_input_token_cost": 2e-08, + "input_cost_per_token": 2e-07, + "litellm_provider": "xai", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "source": "https://docs.x.ai/docs/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "xai/grok-code-fast-1": { + "cache_read_input_token_cost": 2e-08, + "input_cost_per_token": 2e-07, + "litellm_provider": "xai", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "source": "https://docs.x.ai/docs/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "xai/grok-code-fast-1-0825": { + "cache_read_input_token_cost": 2e-08, + "input_cost_per_token": 2e-07, + "litellm_provider": "xai", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 1.5e-06, + "source": "https://docs.x.ai/docs/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "xai/grok-vision-beta": { + "input_cost_per_image": 5e-06, + "input_cost_per_token": 5e-06, + "litellm_provider": "xai", + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true, + "supports_web_search": true + }, + "zai/glm-4.6": { + "input_cost_per_token": 6e-07, + "output_cost_per_token": 2.2e-06, + "litellm_provider": "zai", + "max_input_tokens": 200000, + "max_output_tokens": 128000, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, + "zai/glm-4.5": { + "input_cost_per_token": 6e-07, + "output_cost_per_token": 2.2e-06, + "litellm_provider": "zai", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, + "zai/glm-4.5v": { + "input_cost_per_token": 6e-07, + "output_cost_per_token": 1.8e-06, + "litellm_provider": "zai", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, + "zai/glm-4.5-x": { + "input_cost_per_token": 2.2e-06, + "output_cost_per_token": 8.9e-06, + "litellm_provider": "zai", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, + "zai/glm-4.5-air": { + "input_cost_per_token": 2e-07, + "output_cost_per_token": 1.1e-06, + "litellm_provider": "zai", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, + "zai/glm-4.5-airx": { + "input_cost_per_token": 1.1e-06, + "output_cost_per_token": 4.5e-06, + "litellm_provider": "zai", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, + "zai/glm-4-32b-0414-128k": { + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "zai", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, + "zai/glm-4.5-flash": { + "input_cost_per_token": 0, + "output_cost_per_token": 0, + "litellm_provider": "zai", + "max_input_tokens": 128000, + "max_output_tokens": 32000, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, + "vertex_ai/search_api": { + "input_cost_per_query": 1.5e-03, + "litellm_provider": "vertex_ai", + "mode": "vector_store" + }, + "openai/container": { + "code_interpreter_cost_per_session": 0.03, + "litellm_provider": "openai", + "mode": "chat" + }, + "openai/sora-2": { + "litellm_provider": "openai", + "mode": "video_generation", + "output_cost_per_video_per_second": 0.10, + "source": "https://platform.openai.com/docs/api-reference/videos", + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "video" + ], + "supported_resolutions": [ + "720x1280", + "1280x720" + ] + }, + "openai/sora-2-pro": { + "litellm_provider": "openai", + "mode": "video_generation", + "output_cost_per_video_per_second": 0.30, + "source": "https://platform.openai.com/docs/api-reference/videos", + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "video" + ], + "supported_resolutions": [ + "720x1280", + "1280x720" + ] + }, + "azure/sora-2": { + "litellm_provider": "azure", + "mode": "video_generation", + "output_cost_per_video_per_second": 0.10, + "source": "https://azure.microsoft.com/en-us/products/ai-services/video-generation", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "video" + ], + "supported_resolutions": [ + "720x1280", + "1280x720" + ] + }, + "azure/sora-2-pro": { + "litellm_provider": "azure", + "mode": "video_generation", + "output_cost_per_video_per_second": 0.30, + "source": "https://azure.microsoft.com/en-us/products/ai-services/video-generation", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "video" + ], + "supported_resolutions": [ + "720x1280", + "1280x720" + ] + }, + "azure/sora-2-pro-high-res": { + "litellm_provider": "azure", + "mode": "video_generation", + "output_cost_per_video_per_second": 0.50, + "source": "https://azure.microsoft.com/en-us/products/ai-services/video-generation", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "video" + ], + "supported_resolutions": [ + "1024x1792", + "1792x1024" + ] + }, + "runwayml/gen4_turbo": { + "litellm_provider": "runwayml", + "mode": "video_generation", + "output_cost_per_video_per_second": 0.05, + "source": "https://docs.dev.runwayml.com/guides/pricing/", + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "video" + ], + "supported_resolutions": [ + "1280x720", + "720x1280" + ], + "metadata": { + "comment": "5 credits per second @ $0.01 per credit = $0.05 per second" + } + }, + "runwayml/gen4_aleph": { + "litellm_provider": "runwayml", + "mode": "video_generation", + "output_cost_per_video_per_second": 0.15, + "source": "https://docs.dev.runwayml.com/guides/pricing/", + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "video" + ], + "supported_resolutions": [ + "1280x720", + "720x1280" + ], + "metadata": { + "comment": "15 credits per second @ $0.01 per credit = $0.15 per second" + } + }, + "runwayml/gen3a_turbo": { + "litellm_provider": "runwayml", + "mode": "video_generation", + "output_cost_per_video_per_second": 0.05, + "source": "https://docs.dev.runwayml.com/guides/pricing/", + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "video" + ], + "supported_resolutions": [ + "1280x720", + "720x1280" + ], + "metadata": { + "comment": "5 credits per second @ $0.01 per credit = $0.05 per second" + } + }, + "runwayml/gen4_image": { + "litellm_provider": "runwayml", + "mode": "image_generation", + "input_cost_per_image": 0.05, + "output_cost_per_image": 0.05, + "source": "https://docs.dev.runwayml.com/guides/pricing/", + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "image" + ], + "supported_resolutions": [ + "1280x720", + "1920x1080" + ], + "metadata": { + "comment": "5 credits per 720p image or 8 credits per 1080p image @ $0.01 per credit. Using 5 credits ($0.05) as base cost" + } + }, + "runwayml/gen4_image_turbo": { + "litellm_provider": "runwayml", + "mode": "image_generation", + "input_cost_per_image": 0.02, + "output_cost_per_image": 0.02, + "source": "https://docs.dev.runwayml.com/guides/pricing/", + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "image" + ], + "supported_resolutions": [ + "1280x720", + "1920x1080" + ], + "metadata": { + "comment": "2 credits per image (any resolution) @ $0.01 per credit = $0.02 per image" + } + }, + "runwayml/eleven_multilingual_v2": { + "litellm_provider": "runwayml", + "mode": "audio_speech", + "input_cost_per_character": 3e-07, + "source": "https://docs.dev.runwayml.com/guides/pricing/", + "metadata": { + "comment": "Estimated cost based on standard TTS pricing. RunwayML uses ElevenLabs models." + } + }, + "fireworks_ai/accounts/fireworks/models/qwen3-coder-480b-a35b-instruct": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 4.5e-07, + "output_cost_per_token": 1.8e-06, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/flux-kontext-pro": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 4e-08, + "output_cost_per_token": 4e-08, + "litellm_provider": "fireworks_ai", + "mode": "image_generation" + }, + "fireworks_ai/accounts/fireworks/models/SSD-1B": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 1.3e-10, + "output_cost_per_token": 1.3e-10, + "litellm_provider": "fireworks_ai", + "mode": "image_generation" + }, + "fireworks_ai/accounts/fireworks/models/chronos-hermes-13b-v2": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/code-llama-13b": { + "max_tokens": 16384, + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/code-llama-13b-instruct": { + "max_tokens": 16384, + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/code-llama-13b-python": { + "max_tokens": 16384, + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/code-llama-34b": { + "max_tokens": 16384, + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/code-llama-34b-instruct": { + "max_tokens": 16384, + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/code-llama-34b-python": { + "max_tokens": 16384, + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/code-llama-70b": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/code-llama-70b-instruct": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/code-llama-70b-python": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/code-llama-7b": { + "max_tokens": 16384, + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/code-llama-7b-instruct": { + "max_tokens": 16384, + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/code-llama-7b-python": { + "max_tokens": 16384, + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/code-qwen-1p5-7b": { + "max_tokens": 65536, + "max_input_tokens": 65536, + "max_output_tokens": 65536, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/codegemma-2b": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/codegemma-7b": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/cogito-671b-v2-p1": { + "max_tokens": 163840, + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "input_cost_per_token": 1.2e-06, + "output_cost_per_token": 1.2e-06, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/cogito-v1-preview-llama-3b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/cogito-v1-preview-llama-70b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/cogito-v1-preview-llama-8b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/cogito-v1-preview-qwen-14b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/cogito-v1-preview-qwen-32b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/flux-kontext-max": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 8e-08, + "output_cost_per_token": 8e-08, + "litellm_provider": "fireworks_ai", + "mode": "image_generation" + }, + "fireworks_ai/accounts/fireworks/models/dbrx-instruct": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 1.2e-06, + "output_cost_per_token": 1.2e-06, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/deepseek-coder-1b-base": { + "max_tokens": 16384, + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/deepseek-coder-33b-instruct": { + "max_tokens": 16384, + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/deepseek-coder-7b-base": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/deepseek-coder-7b-base-v1p5": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/deepseek-coder-7b-instruct-v1p5": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/deepseek-coder-v2-lite-base": { + "max_tokens": 163840, + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "input_cost_per_token": 5e-07, + "output_cost_per_token": 5e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/deepseek-coder-v2-lite-instruct": { + "max_tokens": 163840, + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "input_cost_per_token": 5e-07, + "output_cost_per_token": 5e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/deepseek-prover-v2": { + "max_tokens": 163840, + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "input_cost_per_token": 1.2e-06, + "output_cost_per_token": 1.2e-06, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/deepseek-r1-0528-distill-qwen3-8b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/deepseek-r1-distill-llama-70b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/deepseek-r1-distill-llama-8b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/deepseek-r1-distill-qwen-14b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/deepseek-r1-distill-qwen-1p5b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/deepseek-r1-distill-qwen-32b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/deepseek-r1-distill-qwen-7b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/deepseek-v2-lite-chat": { + "max_tokens": 163840, + "max_input_tokens": 163840, + "max_output_tokens": 163840, + "input_cost_per_token": 5e-07, + "output_cost_per_token": 5e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/deepseek-v2p5": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 1.2e-06, + "output_cost_per_token": 1.2e-06, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/devstral-small-2505": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/dobby-mini-unhinged-plus-llama-3-1-8b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/dobby-unhinged-llama-3-3-70b-new": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/dolphin-2-9-2-qwen2-72b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/dolphin-2p6-mixtral-8x7b": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 5e-07, + "output_cost_per_token": 5e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/ernie-4p5-21b-a3b-pt": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/ernie-4p5-300b-a47b-pt": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/fare-20b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/firefunction-v1": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 5e-07, + "output_cost_per_token": 5e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/firellava-13b": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/firesearch-ocr-v6": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/fireworks-asr-large": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 0.0, + "output_cost_per_token": 0.0, + "litellm_provider": "fireworks_ai", + "mode": "audio_transcription" + }, + "fireworks_ai/accounts/fireworks/models/fireworks-asr-v2": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 0.0, + "output_cost_per_token": 0.0, + "litellm_provider": "fireworks_ai", + "mode": "audio_transcription" + }, + "fireworks_ai/accounts/fireworks/models/flux-1-dev": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/flux-1-dev-controlnet-union": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 1e-09, + "output_cost_per_token": 1e-09, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/flux-1-dev-fp8": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 5e-10, + "output_cost_per_token": 5e-10, + "litellm_provider": "fireworks_ai", + "mode": "image_generation" + }, + "fireworks_ai/accounts/fireworks/models/flux-1-schnell": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/flux-1-schnell-fp8": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 3.5e-10, + "output_cost_per_token": 3.5e-10, + "litellm_provider": "fireworks_ai", + "mode": "image_generation" + }, + "fireworks_ai/accounts/fireworks/models/gemma-2b-it": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/gemma-3-27b-it": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/gemma-7b": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/gemma-7b-it": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/gemma2-9b-it": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/glm-4p5v": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 1.2e-06, + "output_cost_per_token": 1.2e-06, + "litellm_provider": "fireworks_ai", + "mode": "chat", + "supports_reasoning": true + }, + "fireworks_ai/accounts/fireworks/models/gpt-oss-safeguard-120b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 1.2e-06, + "output_cost_per_token": 1.2e-06, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/gpt-oss-safeguard-20b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 5e-07, + "output_cost_per_token": 5e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/hermes-2-pro-mistral-7b": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/internvl3-38b": { + "max_tokens": 16384, + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/internvl3-78b": { + "max_tokens": 16384, + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/internvl3-8b": { + "max_tokens": 16384, + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/japanese-stable-diffusion-xl": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 1.3e-10, + "output_cost_per_token": 1.3e-10, + "litellm_provider": "fireworks_ai", + "mode": "image_generation" + }, + "fireworks_ai/accounts/fireworks/models/kat-coder": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/kat-dev-32b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/kat-dev-72b-exp": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/llama-guard-2-8b": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/llama-guard-3-1b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/llama-guard-3-8b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/llama-v2-13b": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/llama-v2-13b-chat": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/llama-v2-70b": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/llama-v2-70b-chat": { + "max_tokens": 2048, + "max_input_tokens": 2048, + "max_output_tokens": 2048, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/llama-v2-7b": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/llama-v2-7b-chat": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/llama-v3-70b-instruct": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/llama-v3-70b-instruct-hf": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/llama-v3-8b": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/llama-v3-8b-instruct-hf": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/llama-v3p1-405b-instruct-long": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/llama-v3p1-70b-instruct": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/llama-v3p1-70b-instruct-1b": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/llama-v3p1-nemotron-70b-instruct": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/llama-v3p2-1b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/llama-v3p2-3b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/llama-v3p3-70b-instruct": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/llamaguard-7b": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/llava-yi-34b": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/minimax-m1-80k": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/minimax-m2": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 3e-07, + "output_cost_per_token": 1.2e-06, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/ministral-3-14b-instruct-2512": { + "max_tokens": 256000, + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/ministral-3-3b-instruct-2512": { + "max_tokens": 256000, + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/ministral-3-8b-instruct-2512": { + "max_tokens": 256000, + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/mistral-7b": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/mistral-7b-instruct-4k": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/mistral-7b-instruct-v0p2": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/mistral-7b-instruct-v3": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/mistral-7b-v0p2": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/mistral-large-3-fp8": { + "max_tokens": 256000, + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "input_cost_per_token": 1.2e-06, + "output_cost_per_token": 1.2e-06, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/mistral-nemo-base-2407": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/mistral-nemo-instruct-2407": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/mistral-small-24b-instruct-2501": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/mixtral-8x22b": { + "max_tokens": 65536, + "max_input_tokens": 65536, + "max_output_tokens": 65536, + "input_cost_per_token": 1.2e-06, + "output_cost_per_token": 1.2e-06, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/mixtral-8x22b-instruct": { + "max_tokens": 65536, + "max_input_tokens": 65536, + "max_output_tokens": 65536, + "input_cost_per_token": 1.2e-06, + "output_cost_per_token": 1.2e-06, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/mixtral-8x7b": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 5e-07, + "output_cost_per_token": 5e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/mixtral-8x7b-instruct": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 5e-07, + "output_cost_per_token": 5e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/mixtral-8x7b-instruct-hf": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 5e-07, + "output_cost_per_token": 5e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/mythomax-l2-13b": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/nemotron-nano-v2-12b-vl": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/nous-capybara-7b-v1p9": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/nous-hermes-2-mixtral-8x7b-dpo": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 5e-07, + "output_cost_per_token": 5e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/nous-hermes-2-yi-34b": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/nous-hermes-llama2-13b": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/nous-hermes-llama2-70b": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/nous-hermes-llama2-7b": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/nvidia-nemotron-nano-12b-v2": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/nvidia-nemotron-nano-9b-v2": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/openchat-3p5-0106-7b": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/openhermes-2-mistral-7b": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/openhermes-2p5-mistral-7b": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/openorca-7b": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/phi-2-3b": { + "max_tokens": 2048, + "max_input_tokens": 2048, + "max_output_tokens": 2048, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/phi-3-mini-128k-instruct": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/phi-3-vision-128k-instruct": { + "max_tokens": 32064, + "max_input_tokens": 32064, + "max_output_tokens": 32064, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/phind-code-llama-34b-python-v1": { + "max_tokens": 16384, + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/phind-code-llama-34b-v1": { + "max_tokens": 16384, + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/phind-code-llama-34b-v2": { + "max_tokens": 16384, + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/playground-v2-1024px-aesthetic": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 1.3e-10, + "output_cost_per_token": 1.3e-10, + "litellm_provider": "fireworks_ai", + "mode": "image_generation" + }, + "fireworks_ai/accounts/fireworks/models/playground-v2-5-1024px-aesthetic": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 1.3e-10, + "output_cost_per_token": 1.3e-10, + "litellm_provider": "fireworks_ai", + "mode": "image_generation" + }, + "fireworks_ai/accounts/fireworks/models/pythia-12b": { + "max_tokens": 2048, + "max_input_tokens": 2048, + "max_output_tokens": 2048, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen-qwq-32b-preview": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen-v2p5-14b-instruct": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen-v2p5-7b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen1p5-72b-chat": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2-7b-instruct": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2-vl-2b-instruct": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2-vl-72b-instruct": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2-vl-7b-instruct": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-0p5b-instruct": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-14b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-1p5b-instruct": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-32b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-32b-instruct": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-72b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-72b-instruct": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-7b-instruct": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-coder-0p5b": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-coder-0p5b-instruct": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-coder-14b": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-coder-14b-instruct": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-coder-1p5b": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-coder-1p5b-instruct": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-coder-32b": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-coder-32b-instruct-128k": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-coder-32b-instruct-32k-rope": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-coder-32b-instruct-64k": { + "max_tokens": 65536, + "max_input_tokens": 65536, + "max_output_tokens": 65536, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-coder-3b": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-coder-3b-instruct": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-coder-7b": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-coder-7b-instruct": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-math-72b-instruct": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-vl-32b-instruct": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-vl-3b-instruct": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-vl-72b-instruct": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen2p5-vl-7b-instruct": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-0p6b": { + "max_tokens": 40960, + "max_input_tokens": 40960, + "max_output_tokens": 40960, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-14b": { + "max_tokens": 40960, + "max_input_tokens": 40960, + "max_output_tokens": 40960, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-1p7b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-1p7b-fp8-draft": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-1p7b-fp8-draft-131072": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-1p7b-fp8-draft-40960": { + "max_tokens": 40960, + "max_input_tokens": 40960, + "max_output_tokens": 40960, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-235b-a22b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 2.2e-07, + "output_cost_per_token": 8.8e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-235b-a22b-instruct-2507": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 2.2e-07, + "output_cost_per_token": 8.8e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-235b-a22b-thinking-2507": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 2.2e-07, + "output_cost_per_token": 8.8e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-30b-a3b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 1.5e-07, + "output_cost_per_token": 6e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-30b-a3b-instruct-2507": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 5e-07, + "output_cost_per_token": 5e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-30b-a3b-thinking-2507": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-32b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-4b": { + "max_tokens": 40960, + "max_input_tokens": 40960, + "max_output_tokens": 40960, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-4b-instruct-2507": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-8b": { + "max_tokens": 40960, + "max_input_tokens": 40960, + "max_output_tokens": 40960, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-coder-30b-a3b-instruct": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 1.5e-07, + "output_cost_per_token": 6e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-coder-480b-instruct-bf16": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-embedding-0p6b": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 0.0, + "output_cost_per_token": 0.0, + "litellm_provider": "fireworks_ai", + "mode": "embedding" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-embedding-4b": { + "max_tokens": 40960, + "max_input_tokens": 40960, + "max_output_tokens": 40960, + "input_cost_per_token": 0.0, + "output_cost_per_token": 0.0, + "litellm_provider": "fireworks_ai", + "mode": "embedding" + }, + "fireworks_ai/accounts/fireworks/models/": { + "max_tokens": 40960, + "max_input_tokens": 40960, + "max_output_tokens": 40960, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 0.0, + "litellm_provider": "fireworks_ai", + "mode": "embedding" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-next-80b-a3b-instruct": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-next-80b-a3b-thinking": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-reranker-0p6b": { + "max_tokens": 40960, + "max_input_tokens": 40960, + "max_output_tokens": 40960, + "input_cost_per_token": 0.0, + "output_cost_per_token": 0.0, + "litellm_provider": "fireworks_ai", + "mode": "rerank" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-reranker-4b": { + "max_tokens": 40960, + "max_input_tokens": 40960, + "max_output_tokens": 40960, + "input_cost_per_token": 0.0, + "output_cost_per_token": 0.0, + "litellm_provider": "fireworks_ai", + "mode": "rerank" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-reranker-8b": { + "max_tokens": 40960, + "max_input_tokens": 40960, + "max_output_tokens": 40960, + "input_cost_per_token": 0.0, + "output_cost_per_token": 0.0, + "litellm_provider": "fireworks_ai", + "mode": "rerank" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-vl-235b-a22b-instruct": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 2.2e-07, + "output_cost_per_token": 8.8e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-vl-235b-a22b-thinking": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 2.2e-07, + "output_cost_per_token": 8.8e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-vl-30b-a3b-instruct": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 1.5e-07, + "output_cost_per_token": 6e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-vl-30b-a3b-thinking": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 1.5e-07, + "output_cost_per_token": 6e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-vl-32b-instruct": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwen3-vl-8b-instruct": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/qwq-32b": { + "max_tokens": 131072, + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/rolm-ocr": { + "max_tokens": 128000, + "max_input_tokens": 128000, + "max_output_tokens": 128000, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/snorkel-mistral-7b-pairrm-dpo": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/stable-diffusion-xl-1024-v1-0": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 1.3e-10, + "output_cost_per_token": 1.3e-10, + "litellm_provider": "fireworks_ai", + "mode": "image_generation" + }, + "fireworks_ai/accounts/fireworks/models/stablecode-3b": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/starcoder-16b": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/starcoder-7b": { + "max_tokens": 8192, + "max_input_tokens": 8192, + "max_output_tokens": 8192, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/starcoder2-15b": { + "max_tokens": 16384, + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/starcoder2-3b": { + "max_tokens": 16384, + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "input_cost_per_token": 1e-07, + "output_cost_per_token": 1e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/starcoder2-7b": { + "max_tokens": 16384, + "max_input_tokens": 16384, + "max_output_tokens": 16384, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/toppy-m-7b": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/whisper-v3": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 0.0, + "output_cost_per_token": 0.0, + "litellm_provider": "fireworks_ai", + "mode": "audio_transcription" + }, + "fireworks_ai/accounts/fireworks/models/whisper-v3-turbo": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 0.0, + "output_cost_per_token": 0.0, + "litellm_provider": "fireworks_ai", + "mode": "audio_transcription" + }, + "fireworks_ai/accounts/fireworks/models/yi-34b": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/yi-34b-200k-capybara": { + "max_tokens": 200000, + "max_input_tokens": 200000, + "max_output_tokens": 200000, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/yi-34b-chat": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 9e-07, + "output_cost_per_token": 9e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/yi-6b": { + "max_tokens": 4096, + "max_input_tokens": 4096, + "max_output_tokens": 4096, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + }, + "fireworks_ai/accounts/fireworks/models/zephyr-7b-beta": { + "max_tokens": 32768, + "max_input_tokens": 32768, + "max_output_tokens": 32768, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 2e-07, + "litellm_provider": "fireworks_ai", + "mode": "chat" + } +} diff --git a/deploy/.env.example b/deploy/.env.example new file mode 100644 index 00000000..be991857 --- /dev/null +++ b/deploy/.env.example @@ -0,0 +1,55 @@ +# ============================================================================= +# Sub2API Docker Environment Configuration +# ============================================================================= +# Copy this file to .env and modify as needed: +# cp .env.example .env +# nano .env +# +# Then start with: docker-compose up -d +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Server Configuration +# ----------------------------------------------------------------------------- +# Bind address for host port mapping +BIND_HOST=0.0.0.0 + +# Server port (exposed on host) +SERVER_PORT=8080 + +# Server mode: release or debug +SERVER_MODE=release + +# Timezone +TZ=Asia/Shanghai + +# ----------------------------------------------------------------------------- +# PostgreSQL Configuration (REQUIRED) +# ----------------------------------------------------------------------------- +POSTGRES_USER=sub2api +POSTGRES_PASSWORD=change_this_secure_password +POSTGRES_DB=sub2api + +# ----------------------------------------------------------------------------- +# Redis Configuration +# ----------------------------------------------------------------------------- +# Leave empty for no password (default for local development) +REDIS_PASSWORD= +REDIS_DB=0 + +# ----------------------------------------------------------------------------- +# Admin Account +# ----------------------------------------------------------------------------- +# Email for the admin account +ADMIN_EMAIL=admin@sub2api.local + +# Password for admin account +# Leave empty to auto-generate (will be shown in logs on first run) +ADMIN_PASSWORD= + +# ----------------------------------------------------------------------------- +# JWT Configuration +# ----------------------------------------------------------------------------- +# Leave empty to auto-generate (recommended) +JWT_SECRET= +JWT_EXPIRE_HOUR=24 diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 00000000..f110451b --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,258 @@ +# Sub2API Deployment Files + +This directory contains files for deploying Sub2API on Linux servers. + +## Deployment Methods + +| Method | Best For | Setup Wizard | +|--------|----------|--------------| +| **Docker Compose** | Quick setup, all-in-one | Not needed (auto-setup) | +| **Binary Install** | Production servers, systemd | Web-based wizard | + +## Files + +| File | Description | +|------|-------------| +| `docker-compose.yml` | Docker Compose configuration | +| `.env.example` | Docker environment variables template | +| `DOCKER.md` | Docker Hub documentation | +| `install.sh` | One-click binary installation script | +| `sub2api.service` | Systemd service unit file | +| `config.example.yaml` | Example configuration file | + +--- + +## Docker Deployment (Recommended) + +### Quick Start + +```bash +# Clone repository +git clone https://github.com/Wei-Shaw/sub2api.git +cd sub2api/deploy + +# Configure environment +cp .env.example .env +nano .env # Set POSTGRES_PASSWORD (required) + +# Start all services +docker-compose up -d + +# View logs (check for auto-generated admin password) +docker-compose logs -f sub2api + +# Access Web UI +# http://localhost:8080 +``` + +### How Auto-Setup Works + +When using Docker Compose with `AUTO_SETUP=true`: + +1. On first run, the system automatically: + - Connects to PostgreSQL and Redis + - Creates all database tables + - Generates JWT secret (if not provided) + - Creates admin account (password auto-generated if not provided) + - Writes config.yaml + +2. No manual Setup Wizard needed - just configure `.env` and start + +3. If `ADMIN_PASSWORD` is not set, check logs for the generated password: + ```bash + docker-compose logs sub2api | grep "admin password" + ``` + +### Commands + +```bash +# Start services +docker-compose up -d + +# Stop services +docker-compose down + +# View logs +docker-compose logs -f sub2api + +# Restart Sub2API only +docker-compose restart sub2api + +# Update to latest version +docker-compose pull +docker-compose up -d + +# Remove all data (caution!) +docker-compose down -v +``` + +### Environment Variables + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `POSTGRES_PASSWORD` | **Yes** | - | PostgreSQL password | +| `SERVER_PORT` | No | `8080` | Server port | +| `ADMIN_EMAIL` | No | `admin@sub2api.local` | Admin email | +| `ADMIN_PASSWORD` | No | *(auto-generated)* | Admin password | +| `JWT_SECRET` | No | *(auto-generated)* | JWT secret | +| `TZ` | No | `Asia/Shanghai` | Timezone | + +See `.env.example` for all available options. + +--- + +## Binary Installation + +For production servers using systemd. + +### One-Line Installation + +```bash +curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install.sh | sudo bash +``` + +### Manual Installation + +1. Download the latest release from [GitHub Releases](https://github.com/Wei-Shaw/sub2api/releases) +2. Extract and copy the binary to `/opt/sub2api/` +3. Copy `sub2api.service` to `/etc/systemd/system/` +4. Run: + ```bash + sudo systemctl daemon-reload + sudo systemctl enable sub2api + sudo systemctl start sub2api + ``` +5. Open the Setup Wizard in your browser to complete configuration + +### Commands + +```bash +# Install +sudo ./install.sh + +# Upgrade +sudo ./install.sh upgrade + +# Uninstall +sudo ./install.sh uninstall +``` + +### Service Management + +```bash +# Start the service +sudo systemctl start sub2api + +# Stop the service +sudo systemctl stop sub2api + +# Restart the service +sudo systemctl restart sub2api + +# Check status +sudo systemctl status sub2api + +# View logs +sudo journalctl -u sub2api -f + +# Enable auto-start on boot +sudo systemctl enable sub2api +``` + +### Configuration + +#### Server Address and Port + +During installation, you will be prompted to configure the server listen address and port. These settings are stored in the systemd service file as environment variables. + +To change after installation: + +1. Edit the systemd service: + ```bash + sudo systemctl edit sub2api + ``` + +2. Add or modify: + ```ini + [Service] + Environment=SERVER_HOST=0.0.0.0 + Environment=SERVER_PORT=3000 + ``` + +3. Reload and restart: + ```bash + sudo systemctl daemon-reload + sudo systemctl restart sub2api + ``` + +#### Application Configuration + +The main config file is at `/etc/sub2api/config.yaml` (created by Setup Wizard). + +### Prerequisites + +- Linux server (Ubuntu 20.04+, Debian 11+, CentOS 8+, etc.) +- PostgreSQL 14+ +- Redis 6+ +- systemd + +### Directory Structure + +``` +/opt/sub2api/ +├── sub2api # Main binary +├── sub2api.backup # Backup (after upgrade) +└── data/ # Runtime data + +/etc/sub2api/ +└── config.yaml # Configuration file +``` + +--- + +## Troubleshooting + +### Docker + +```bash +# Check container status +docker-compose ps + +# View detailed logs +docker-compose logs --tail=100 sub2api + +# Check database connection +docker-compose exec postgres pg_isready + +# Check Redis connection +docker-compose exec redis redis-cli ping + +# Restart all services +docker-compose restart +``` + +### Binary Install + +```bash +# Check service status +sudo systemctl status sub2api + +# View recent logs +sudo journalctl -u sub2api -n 50 + +# Check config file +sudo cat /etc/sub2api/config.yaml + +# Check PostgreSQL +sudo systemctl status postgresql + +# Check Redis +sudo systemctl status redis +``` + +### Common Issues + +1. **Port already in use**: Change `SERVER_PORT` in `.env` or systemd config +2. **Database connection failed**: Check PostgreSQL is running and credentials are correct +3. **Redis connection failed**: Check Redis is running and password is correct +4. **Permission denied**: Ensure proper file ownership for binary install diff --git a/deploy/config.example.yaml b/deploy/config.example.yaml new file mode 100644 index 00000000..1e466244 --- /dev/null +++ b/deploy/config.example.yaml @@ -0,0 +1,89 @@ +# Sub2API Configuration File +# Copy this file to /etc/sub2api/config.yaml and modify as needed +# Documentation: https://github.com/Wei-Shaw/sub2api + +# ============================================================================= +# Server Configuration +# ============================================================================= +server: + # Bind address (0.0.0.0 for all interfaces) + host: "0.0.0.0" + # Port to listen on + port: 8080 + # Mode: "debug" for development, "release" for production + mode: "release" + +# ============================================================================= +# Database Configuration (PostgreSQL) +# ============================================================================= +database: + host: "localhost" + port: 5432 + user: "postgres" + password: "your_secure_password_here" + dbname: "sub2api" + # SSL mode: disable, require, verify-ca, verify-full + sslmode: "disable" + +# ============================================================================= +# Redis Configuration +# ============================================================================= +redis: + host: "localhost" + port: 6379 + # Leave empty if no password is set + password: "" + # Database number (0-15) + db: 0 + +# ============================================================================= +# JWT Configuration +# ============================================================================= +jwt: + # IMPORTANT: Change this to a random string in production! + # Generate with: openssl rand -hex 32 + secret: "change-this-to-a-secure-random-string" + # Token expiration time in hours + expire_hour: 24 + +# ============================================================================= +# Default Settings +# ============================================================================= +default: + # Initial admin account (created on first run) + admin_email: "admin@example.com" + admin_password: "admin123" + + # Default settings for new users + user_concurrency: 5 # Max concurrent requests per user + user_balance: 0 # Initial balance for new users + + # API key settings + api_key_prefix: "sk-" # Prefix for generated API keys + + # Rate multiplier (affects billing calculation) + rate_multiplier: 1.0 + +# ============================================================================= +# Rate Limiting +# ============================================================================= +rate_limit: + # Cooldown time (in minutes) when upstream returns 529 (overloaded) + overload_cooldown_minutes: 10 + +# ============================================================================= +# Pricing Data Source (Optional) +# ============================================================================= +pricing: + # URL to fetch model pricing data (default: LiteLLM) + remote_url: "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json" + # Hash verification URL (optional) + hash_url: "" + # Local data directory for caching + data_dir: "./data" + # Fallback pricing file + fallback_file: "./resources/model-pricing/model_prices_and_context_window.json" + # Update interval in hours + update_interval_hours: 24 + # Hash check interval in minutes + hash_check_interval_minutes: 10 diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 00000000..b28ec584 --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,160 @@ +# ============================================================================= +# Sub2API Docker Compose Configuration +# ============================================================================= +# Quick Start: +# 1. Copy .env.example to .env and configure +# 2. docker-compose up -d +# 3. Check logs: docker-compose logs -f sub2api +# 4. Access: http://localhost:8080 +# +# All configuration is done via environment variables. +# No Setup Wizard needed - the system auto-initializes on first run. +# ============================================================================= + +services: + # =========================================================================== + # Sub2API Application + # =========================================================================== + sub2api: + image: weishaw/sub2api:latest + container_name: sub2api + restart: unless-stopped + ports: + - "${BIND_HOST:-0.0.0.0}:${SERVER_PORT:-8080}:8080" + volumes: + # Data persistence (config.yaml will be auto-generated here) + - sub2api_data:/app/data + environment: + # ======================================================================= + # Auto Setup (REQUIRED for Docker deployment) + # ======================================================================= + - AUTO_SETUP=true + + # ======================================================================= + # Server Configuration + # ======================================================================= + - SERVER_HOST=0.0.0.0 + - SERVER_PORT=8080 + - SERVER_MODE=${SERVER_MODE:-release} + + # ======================================================================= + # Database Configuration (PostgreSQL) + # ======================================================================= + - DATABASE_HOST=postgres + - DATABASE_PORT=5432 + - DATABASE_USER=${POSTGRES_USER:-sub2api} + - DATABASE_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required} + - DATABASE_DBNAME=${POSTGRES_DB:-sub2api} + - DATABASE_SSLMODE=disable + + # ======================================================================= + # Redis Configuration + # ======================================================================= + - REDIS_HOST=redis + - REDIS_PORT=6379 + - REDIS_PASSWORD=${REDIS_PASSWORD:-} + - REDIS_DB=${REDIS_DB:-0} + + # ======================================================================= + # Admin Account (auto-created on first run) + # ======================================================================= + - ADMIN_EMAIL=${ADMIN_EMAIL:-admin@sub2api.local} + - ADMIN_PASSWORD=${ADMIN_PASSWORD:-} + + # ======================================================================= + # JWT Configuration + # ======================================================================= + # Leave empty to auto-generate (recommended) + - JWT_SECRET=${JWT_SECRET:-} + - JWT_EXPIRE_HOUR=${JWT_EXPIRE_HOUR:-24} + + # ======================================================================= + # Timezone Configuration + # This affects ALL time operations in the application: + # - Database timestamps + # - Usage statistics "today" boundary + # - Subscription expiry times + # - Log timestamps + # Common values: Asia/Shanghai, America/New_York, Europe/London, UTC + # ======================================================================= + - TZ=${TZ:-Asia/Shanghai} + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + networks: + - sub2api-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + + # =========================================================================== + # PostgreSQL Database + # =========================================================================== + postgres: + image: postgres:15-alpine + container_name: sub2api-postgres + restart: unless-stopped + volumes: + - postgres_data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=${POSTGRES_USER:-sub2api} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required} + - POSTGRES_DB=${POSTGRES_DB:-sub2api} + - TZ=${TZ:-Asia/Shanghai} + networks: + - sub2api-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-sub2api} -d ${POSTGRES_DB:-sub2api}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + # =========================================================================== + # Redis Cache + # =========================================================================== + redis: + image: redis:7-alpine + container_name: sub2api-redis + restart: unless-stopped + volumes: + - redis_data:/data + command: > + redis-server + --save 60 1 + --appendonly yes + --appendfsync everysec + ${REDIS_PASSWORD:+--requirepass ${REDIS_PASSWORD}} + environment: + - TZ=${TZ:-Asia/Shanghai} + networks: + - sub2api-network + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 5s + +# ============================================================================= +# Volumes +# ============================================================================= +volumes: + sub2api_data: + driver: local + postgres_data: + driver: local + redis_data: + driver: local + +# ============================================================================= +# Networks +# ============================================================================= +networks: + sub2api-network: + driver: bridge diff --git a/deploy/install.sh b/deploy/install.sh new file mode 100644 index 00000000..ea00e6fe --- /dev/null +++ b/deploy/install.sh @@ -0,0 +1,745 @@ +#!/bin/bash +# +# Sub2API Installation Script +# Sub2API 安装脚本 +# Usage: curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install.sh | bash +# + +set -e + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# Configuration +GITHUB_REPO="Wei-Shaw/sub2api" +INSTALL_DIR="/opt/sub2api" +SERVICE_NAME="sub2api" +SERVICE_USER="sub2api" +CONFIG_DIR="/etc/sub2api" + +# Server configuration (will be set by user) +SERVER_HOST="0.0.0.0" +SERVER_PORT="8080" + +# Language (default: zh = Chinese) +LANG_CHOICE="zh" + +# ============================================================ +# Language strings / 语言字符串 +# ============================================================ + +# Chinese strings +declare -A MSG_ZH=( + # General + ["info"]="信息" + ["success"]="成功" + ["warning"]="警告" + ["error"]="错误" + + # Language selection + ["select_lang"]="请选择语言 / Select language" + ["lang_zh"]="中文" + ["lang_en"]="English" + ["enter_choice"]="请输入选择 (默认: 1)" + + # Installation + ["install_title"]="Sub2API 安装脚本" + ["run_as_root"]="请使用 root 权限运行 (使用 sudo)" + ["detected_platform"]="检测到平台" + ["unsupported_arch"]="不支持的架构" + ["unsupported_os"]="不支持的操作系统" + ["missing_deps"]="缺少依赖" + ["install_deps_first"]="请先安装以下依赖" + ["fetching_version"]="正在获取最新版本..." + ["latest_version"]="最新版本" + ["failed_get_version"]="获取最新版本失败" + ["downloading"]="正在下载" + ["download_failed"]="下载失败" + ["verifying_checksum"]="正在校验文件..." + ["checksum_verified"]="校验通过" + ["checksum_failed"]="校验失败" + ["checksum_not_found"]="无法验证校验和(checksums.txt 未找到)" + ["extracting"]="正在解压..." + ["binary_installed"]="二进制文件已安装到" + ["user_exists"]="用户已存在" + ["creating_user"]="正在创建系统用户" + ["user_created"]="用户已创建" + ["setting_up_dirs"]="正在设置目录..." + ["dirs_configured"]="目录配置完成" + ["installing_service"]="正在安装 systemd 服务..." + ["service_installed"]="systemd 服务已安装" + ["setting_up_sudoers"]="正在配置 sudoers..." + ["sudoers_configured"]="sudoers 配置完成" + ["sudoers_failed"]="sudoers 验证失败,已移除文件" + ["ready_for_setup"]="准备就绪,可以启动设置向导" + + # Completion + ["install_complete"]="Sub2API 安装完成!" + ["install_dir"]="安装目录" + ["next_steps"]="后续步骤" + ["step1_check_services"]="确保 PostgreSQL 和 Redis 正在运行:" + ["step2_start_service"]="启动 Sub2API 服务:" + ["step3_enable_autostart"]="设置开机自启:" + ["step4_open_wizard"]="在浏览器中打开设置向导:" + ["wizard_guide"]="设置向导将引导您完成:" + ["wizard_db"]="数据库配置" + ["wizard_redis"]="Redis 配置" + ["wizard_admin"]="管理员账号创建" + ["useful_commands"]="常用命令" + ["cmd_status"]="查看状态" + ["cmd_logs"]="查看日志" + ["cmd_restart"]="重启服务" + ["cmd_stop"]="停止服务" + + # Upgrade + ["upgrading"]="正在升级 Sub2API..." + ["current_version"]="当前版本" + ["stopping_service"]="正在停止服务..." + ["backup_created"]="备份已创建" + ["starting_service"]="正在启动服务..." + ["upgrade_complete"]="升级完成!" + + # Uninstall + ["uninstall_confirm"]="这将从系统中移除 Sub2API。" + ["are_you_sure"]="确定要继续吗?(y/N)" + ["uninstall_cancelled"]="卸载已取消" + ["removing_files"]="正在移除文件..." + ["removing_install_dir"]="正在移除安装目录..." + ["removing_user"]="正在移除用户..." + ["config_not_removed"]="配置目录未被移除" + ["remove_manually"]="如不再需要,请手动删除" + ["uninstall_complete"]="Sub2API 已卸载" + + # Help + ["usage"]="用法" + ["cmd_none"]="(无参数)" + ["cmd_install"]="安装 Sub2API" + ["cmd_upgrade"]="升级到最新版本" + ["cmd_uninstall"]="卸载 Sub2API" + + # Server configuration + ["server_config_title"]="服务器配置" + ["server_config_desc"]="配置 Sub2API 服务监听地址" + ["server_host_prompt"]="服务器监听地址" + ["server_host_hint"]="0.0.0.0 表示监听所有网卡,127.0.0.1 仅本地访问" + ["server_port_prompt"]="服务器端口" + ["server_port_hint"]="建议使用 1024-65535 之间的端口" + ["server_config_summary"]="服务器配置" + ["invalid_port"]="无效端口号,请输入 1-65535 之间的数字" +) + +# English strings +declare -A MSG_EN=( + # General + ["info"]="INFO" + ["success"]="SUCCESS" + ["warning"]="WARNING" + ["error"]="ERROR" + + # Language selection + ["select_lang"]="请选择语言 / Select language" + ["lang_zh"]="中文" + ["lang_en"]="English" + ["enter_choice"]="Enter your choice (default: 1)" + + # Installation + ["install_title"]="Sub2API Installation Script" + ["run_as_root"]="Please run as root (use sudo)" + ["detected_platform"]="Detected platform" + ["unsupported_arch"]="Unsupported architecture" + ["unsupported_os"]="Unsupported OS" + ["missing_deps"]="Missing dependencies" + ["install_deps_first"]="Please install them first" + ["fetching_version"]="Fetching latest version..." + ["latest_version"]="Latest version" + ["failed_get_version"]="Failed to get latest version" + ["downloading"]="Downloading" + ["download_failed"]="Download failed" + ["verifying_checksum"]="Verifying checksum..." + ["checksum_verified"]="Checksum verified" + ["checksum_failed"]="Checksum verification failed" + ["checksum_not_found"]="Could not verify checksum (checksums.txt not found)" + ["extracting"]="Extracting..." + ["binary_installed"]="Binary installed to" + ["user_exists"]="User already exists" + ["creating_user"]="Creating system user" + ["user_created"]="User created" + ["setting_up_dirs"]="Setting up directories..." + ["dirs_configured"]="Directories configured" + ["installing_service"]="Installing systemd service..." + ["service_installed"]="Systemd service installed" + ["setting_up_sudoers"]="Setting up sudoers..." + ["sudoers_configured"]="Sudoers configured" + ["sudoers_failed"]="Sudoers validation failed, removing file" + ["ready_for_setup"]="Ready for Setup Wizard" + + # Completion + ["install_complete"]="Sub2API installation completed!" + ["install_dir"]="Installation directory" + ["next_steps"]="NEXT STEPS" + ["step1_check_services"]="Make sure PostgreSQL and Redis are running:" + ["step2_start_service"]="Start Sub2API service:" + ["step3_enable_autostart"]="Enable auto-start on boot:" + ["step4_open_wizard"]="Open the Setup Wizard in your browser:" + ["wizard_guide"]="The Setup Wizard will guide you through:" + ["wizard_db"]="Database configuration" + ["wizard_redis"]="Redis configuration" + ["wizard_admin"]="Admin account creation" + ["useful_commands"]="USEFUL COMMANDS" + ["cmd_status"]="Check status" + ["cmd_logs"]="View logs" + ["cmd_restart"]="Restart" + ["cmd_stop"]="Stop" + + # Upgrade + ["upgrading"]="Upgrading Sub2API..." + ["current_version"]="Current version" + ["stopping_service"]="Stopping service..." + ["backup_created"]="Backup created" + ["starting_service"]="Starting service..." + ["upgrade_complete"]="Upgrade completed!" + + # Uninstall + ["uninstall_confirm"]="This will remove Sub2API from your system." + ["are_you_sure"]="Are you sure? (y/N)" + ["uninstall_cancelled"]="Uninstall cancelled" + ["removing_files"]="Removing files..." + ["removing_install_dir"]="Removing installation directory..." + ["removing_user"]="Removing user..." + ["config_not_removed"]="Config directory was NOT removed." + ["remove_manually"]="Remove it manually if you no longer need it." + ["uninstall_complete"]="Sub2API has been uninstalled" + + # Help + ["usage"]="Usage" + ["cmd_none"]="(none)" + ["cmd_install"]="Install Sub2API" + ["cmd_upgrade"]="Upgrade to the latest version" + ["cmd_uninstall"]="Remove Sub2API" + + # Server configuration + ["server_config_title"]="Server Configuration" + ["server_config_desc"]="Configure Sub2API server listen address" + ["server_host_prompt"]="Server listen address" + ["server_host_hint"]="0.0.0.0 listens on all interfaces, 127.0.0.1 for local only" + ["server_port_prompt"]="Server port" + ["server_port_hint"]="Recommended range: 1024-65535" + ["server_config_summary"]="Server configuration" + ["invalid_port"]="Invalid port number, please enter a number between 1-65535" +) + +# Get message based on current language +msg() { + local key="$1" + if [ "$LANG_CHOICE" = "en" ]; then + echo "${MSG_EN[$key]}" + else + echo "${MSG_ZH[$key]}" + fi +} + +# Print functions +print_info() { + echo -e "${BLUE}[$(msg 'info')]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[$(msg 'success')]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[$(msg 'warning')]${NC} $1" +} + +print_error() { + echo -e "${RED}[$(msg 'error')]${NC} $1" +} + +# Select language +select_language() { + echo "" + echo -e "${CYAN}==============================================" + echo " $(msg 'select_lang')" + echo "==============================================${NC}" + echo "" + echo " 1) $(msg 'lang_zh') (默认/default)" + echo " 2) $(msg 'lang_en')" + echo "" + + # Read with timeout for piped input + read -t 10 -p "$(msg 'enter_choice'): " lang_input 2>/dev/null || lang_input="" + + case "$lang_input" in + 2|en|EN|english|English) + LANG_CHOICE="en" + ;; + *) + LANG_CHOICE="zh" + ;; + esac + + echo "" +} + +# Validate port number +validate_port() { + local port="$1" + if [[ "$port" =~ ^[0-9]+$ ]] && [ "$port" -ge 1 ] && [ "$port" -le 65535 ]; then + return 0 + fi + return 1 +} + +# Configure server settings +configure_server() { + echo "" + echo -e "${CYAN}==============================================" + echo " $(msg 'server_config_title')" + echo "==============================================${NC}" + echo "" + echo -e "${BLUE}$(msg 'server_config_desc')${NC}" + echo "" + + # Server host + echo -e "${YELLOW}$(msg 'server_host_hint')${NC}" + read -p "$(msg 'server_host_prompt') [${SERVER_HOST}]: " input_host + if [ -n "$input_host" ]; then + SERVER_HOST="$input_host" + fi + + echo "" + + # Server port + echo -e "${YELLOW}$(msg 'server_port_hint')${NC}" + while true; do + read -p "$(msg 'server_port_prompt') [${SERVER_PORT}]: " input_port + if [ -z "$input_port" ]; then + # Use default + break + elif validate_port "$input_port"; then + SERVER_PORT="$input_port" + break + else + print_error "$(msg 'invalid_port')" + fi + done + + echo "" + print_info "$(msg 'server_config_summary'): ${SERVER_HOST}:${SERVER_PORT}" + echo "" +} + +# Check if running as root +check_root() { + if [ "$EUID" -ne 0 ]; then + print_error "$(msg 'run_as_root')" + exit 1 + fi +} + +# Detect OS and architecture +detect_platform() { + OS=$(uname -s | tr '[:upper:]' '[:lower:]') + ARCH=$(uname -m) + + case "$ARCH" in + x86_64) + ARCH="amd64" + ;; + aarch64|arm64) + ARCH="arm64" + ;; + *) + print_error "$(msg 'unsupported_arch'): $ARCH" + exit 1 + ;; + esac + + case "$OS" in + linux) + OS="linux" + ;; + darwin) + OS="darwin" + ;; + *) + print_error "$(msg 'unsupported_os'): $OS" + exit 1 + ;; + esac + + print_info "$(msg 'detected_platform'): ${OS}_${ARCH}" +} + +# Check dependencies +check_dependencies() { + local missing=() + + if ! command -v curl &> /dev/null; then + missing+=("curl") + fi + + if ! command -v tar &> /dev/null; then + missing+=("tar") + fi + + if [ ${#missing[@]} -gt 0 ]; then + print_error "$(msg 'missing_deps'): ${missing[*]}" + print_info "$(msg 'install_deps_first')" + exit 1 + fi +} + +# Get latest release version +get_latest_version() { + print_info "$(msg 'fetching_version')" + LATEST_VERSION=$(curl -s "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') + + if [ -z "$LATEST_VERSION" ]; then + print_error "$(msg 'failed_get_version')" + exit 1 + fi + + print_info "$(msg 'latest_version'): $LATEST_VERSION" +} + +# Download and extract +download_and_extract() { + local version_num=${LATEST_VERSION#v} + local archive_name="sub2api_${version_num}_${OS}_${ARCH}.tar.gz" + local download_url="https://github.com/${GITHUB_REPO}/releases/download/${LATEST_VERSION}/${archive_name}" + local checksum_url="https://github.com/${GITHUB_REPO}/releases/download/${LATEST_VERSION}/checksums.txt" + + print_info "$(msg 'downloading') ${archive_name}..." + + # Create temp directory + TEMP_DIR=$(mktemp -d) + trap "rm -rf $TEMP_DIR" EXIT + + # Download archive + if ! curl -sL "$download_url" -o "$TEMP_DIR/$archive_name"; then + print_error "$(msg 'download_failed')" + exit 1 + fi + + # Download and verify checksum + print_info "$(msg 'verifying_checksum')" + if curl -sL "$checksum_url" -o "$TEMP_DIR/checksums.txt" 2>/dev/null; then + local expected_checksum=$(grep "$archive_name" "$TEMP_DIR/checksums.txt" | awk '{print $1}') + local actual_checksum=$(sha256sum "$TEMP_DIR/$archive_name" | awk '{print $1}') + + if [ "$expected_checksum" != "$actual_checksum" ]; then + print_error "$(msg 'checksum_failed')" + print_error "Expected: $expected_checksum" + print_error "Actual: $actual_checksum" + exit 1 + fi + print_success "$(msg 'checksum_verified')" + else + print_warning "$(msg 'checksum_not_found')" + fi + + # Extract + print_info "$(msg 'extracting')" + tar -xzf "$TEMP_DIR/$archive_name" -C "$TEMP_DIR" + + # Create install directory + mkdir -p "$INSTALL_DIR" + + # Copy binary + cp "$TEMP_DIR/sub2api" "$INSTALL_DIR/sub2api" + chmod +x "$INSTALL_DIR/sub2api" + + # Copy deploy files if they exist in the archive + if [ -d "$TEMP_DIR/deploy" ]; then + cp -r "$TEMP_DIR/deploy/"* "$INSTALL_DIR/" 2>/dev/null || true + fi + + print_success "$(msg 'binary_installed') $INSTALL_DIR/sub2api" +} + +# Create system user +create_user() { + if id "$SERVICE_USER" &>/dev/null; then + print_info "$(msg 'user_exists'): $SERVICE_USER" + else + print_info "$(msg 'creating_user') $SERVICE_USER..." + useradd -r -s /bin/false -d "$INSTALL_DIR" "$SERVICE_USER" + print_success "$(msg 'user_created')" + fi +} + +# Setup directories and permissions +setup_directories() { + print_info "$(msg 'setting_up_dirs')" + + # Create directories + mkdir -p "$INSTALL_DIR" + mkdir -p "$INSTALL_DIR/data" + mkdir -p "$CONFIG_DIR" + + # Set ownership + chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR" + chown -R "$SERVICE_USER:$SERVICE_USER" "$CONFIG_DIR" + + print_success "$(msg 'dirs_configured')" +} + +# Setup sudoers for service restart +setup_sudoers() { + print_info "$(msg 'setting_up_sudoers')" + + # Check if sudoers file exists in install dir + if [ -f "$INSTALL_DIR/sub2api-sudoers" ]; then + cp "$INSTALL_DIR/sub2api-sudoers" /etc/sudoers.d/sub2api + else + # Create sudoers file + cat > /etc/sudoers.d/sub2api << 'EOF' +# Sudoers configuration for Sub2API +sub2api ALL=(ALL) NOPASSWD: /bin/systemctl restart sub2api +sub2api ALL=(ALL) NOPASSWD: /bin/systemctl stop sub2api +sub2api ALL=(ALL) NOPASSWD: /bin/systemctl start sub2api +EOF + fi + + # Set correct permissions (required for sudoers files) + chmod 440 /etc/sudoers.d/sub2api + + # Validate sudoers file + if visudo -c -f /etc/sudoers.d/sub2api &>/dev/null; then + print_success "$(msg 'sudoers_configured')" + else + print_warning "$(msg 'sudoers_failed')" + rm -f /etc/sudoers.d/sub2api + fi +} + +# Install systemd service +install_service() { + print_info "$(msg 'installing_service')" + + # Create service file with configured host and port + cat > /etc/systemd/system/sub2api.service << EOF +[Unit] +Description=Sub2API - AI API Gateway Platform +Documentation=https://github.com/Wei-Shaw/sub2api +After=network.target postgresql.service redis.service +Wants=postgresql.service redis.service + +[Service] +Type=simple +User=sub2api +Group=sub2api +WorkingDirectory=/opt/sub2api +ExecStart=/opt/sub2api/sub2api +Restart=always +RestartSec=5 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=sub2api + +# Security hardening +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +PrivateTmp=true +ReadWritePaths=/opt/sub2api + +# Environment - Server configuration +Environment=GIN_MODE=release +Environment=SERVER_HOST=${SERVER_HOST} +Environment=SERVER_PORT=${SERVER_PORT} + +[Install] +WantedBy=multi-user.target +EOF + + # Reload systemd + systemctl daemon-reload + + print_success "$(msg 'service_installed')" +} + +# Prepare for setup wizard (no config file needed - setup wizard will create it) +prepare_for_setup() { + print_success "$(msg 'ready_for_setup')" +} + +# Print completion message +print_completion() { + local ip_addr + ip_addr=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "YOUR_SERVER_IP") + + # Determine display address + local display_host="$ip_addr" + if [ "$SERVER_HOST" = "127.0.0.1" ]; then + display_host="127.0.0.1" + fi + + echo "" + echo "==============================================" + print_success "$(msg 'install_complete')" + echo "==============================================" + echo "" + echo "$(msg 'install_dir'): $INSTALL_DIR" + echo "$(msg 'server_config_summary'): ${SERVER_HOST}:${SERVER_PORT}" + echo "" + echo "==============================================" + echo " $(msg 'next_steps')" + echo "==============================================" + echo "" + echo " 1. $(msg 'step1_check_services')" + echo " sudo systemctl status postgresql" + echo " sudo systemctl status redis" + echo "" + echo " 2. $(msg 'step2_start_service')" + echo " sudo systemctl start sub2api" + echo "" + echo " 3. $(msg 'step3_enable_autostart')" + echo " sudo systemctl enable sub2api" + echo "" + echo " 4. $(msg 'step4_open_wizard')" + echo "" + print_info " http://${display_host}:${SERVER_PORT}" + echo "" + echo " $(msg 'wizard_guide')" + echo " - $(msg 'wizard_db')" + echo " - $(msg 'wizard_redis')" + echo " - $(msg 'wizard_admin')" + echo "" + echo "==============================================" + echo " $(msg 'useful_commands')" + echo "==============================================" + echo "" + echo " $(msg 'cmd_status'): sudo systemctl status sub2api" + echo " $(msg 'cmd_logs'): sudo journalctl -u sub2api -f" + echo " $(msg 'cmd_restart'): sudo systemctl restart sub2api" + echo " $(msg 'cmd_stop'): sudo systemctl stop sub2api" + echo "" + echo "==============================================" +} + +# Upgrade function +upgrade() { + print_info "$(msg 'upgrading')" + + # Get current version + if [ -f "$INSTALL_DIR/sub2api" ]; then + CURRENT_VERSION=$("$INSTALL_DIR/sub2api" --version 2>/dev/null | grep -oP 'v?\d+\.\d+\.\d+' || echo "unknown") + print_info "$(msg 'current_version'): $CURRENT_VERSION" + fi + + # Stop service + if systemctl is-active --quiet sub2api; then + print_info "$(msg 'stopping_service')" + systemctl stop sub2api + fi + + # Backup current binary + if [ -f "$INSTALL_DIR/sub2api" ]; then + cp "$INSTALL_DIR/sub2api" "$INSTALL_DIR/sub2api.backup" + print_info "$(msg 'backup_created'): $INSTALL_DIR/sub2api.backup" + fi + + # Download and install new version + get_latest_version + download_and_extract + + # Set permissions + chown "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR/sub2api" + + # Start service + print_info "$(msg 'starting_service')" + systemctl start sub2api + + print_success "$(msg 'upgrade_complete')" +} + +# Uninstall function +uninstall() { + print_warning "$(msg 'uninstall_confirm')" + read -p "$(msg 'are_you_sure') " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + print_info "$(msg 'uninstall_cancelled')" + exit 0 + fi + + print_info "$(msg 'stopping_service')" + systemctl stop sub2api 2>/dev/null || true + systemctl disable sub2api 2>/dev/null || true + + print_info "$(msg 'removing_files')" + rm -f /etc/systemd/system/sub2api.service + rm -f /etc/sudoers.d/sub2api + systemctl daemon-reload + + print_info "$(msg 'removing_install_dir')" + rm -rf "$INSTALL_DIR" + + print_info "$(msg 'removing_user')" + userdel "$SERVICE_USER" 2>/dev/null || true + + print_warning "$(msg 'config_not_removed'): $CONFIG_DIR" + print_warning "$(msg 'remove_manually')" + + print_success "$(msg 'uninstall_complete')" +} + +# Main +main() { + # Select language first + select_language + + echo "" + echo "==============================================" + echo " $(msg 'install_title')" + echo "==============================================" + echo "" + + # Parse arguments + case "${1:-}" in + upgrade|update) + check_root + detect_platform + upgrade + exit 0 + ;; + uninstall|remove) + check_root + uninstall + exit 0 + ;; + --help|-h) + echo "$(msg 'usage'): $0 [command]" + echo "" + echo "Commands:" + echo " $(msg 'cmd_none') $(msg 'cmd_install')" + echo " upgrade $(msg 'cmd_upgrade')" + echo " uninstall $(msg 'cmd_uninstall')" + echo "" + exit 0 + ;; + esac + + # Fresh install + check_root + detect_platform + check_dependencies + configure_server + get_latest_version + download_and_extract + create_user + setup_directories + install_service + setup_sudoers + prepare_for_setup + print_completion +} + +main "$@" diff --git a/deploy/sub2api-sudoers b/deploy/sub2api-sudoers new file mode 100644 index 00000000..8ce6da2c --- /dev/null +++ b/deploy/sub2api-sudoers @@ -0,0 +1,13 @@ +# Sudoers configuration for Sub2API +# This file allows the sub2api service user to restart the service without password +# +# Installation: +# sudo cp sub2api-sudoers /etc/sudoers.d/sub2api +# sudo chmod 440 /etc/sudoers.d/sub2api +# +# SECURITY NOTE: This grants limited sudo access only for service management + +# Allow sub2api user to restart the service without password +sub2api ALL=(ALL) NOPASSWD: /bin/systemctl restart sub2api +sub2api ALL=(ALL) NOPASSWD: /bin/systemctl stop sub2api +sub2api ALL=(ALL) NOPASSWD: /bin/systemctl start sub2api diff --git a/deploy/sub2api.service b/deploy/sub2api.service new file mode 100644 index 00000000..1a59ad03 --- /dev/null +++ b/deploy/sub2api.service @@ -0,0 +1,33 @@ +[Unit] +Description=Sub2API - AI API Gateway Platform +Documentation=https://github.com/Wei-Shaw/sub2api +After=network.target postgresql.service redis.service +Wants=postgresql.service redis.service + +[Service] +Type=simple +User=sub2api +Group=sub2api +WorkingDirectory=/opt/sub2api +ExecStart=/opt/sub2api/sub2api +Restart=always +RestartSec=5 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=sub2api + +# Security hardening +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +PrivateTmp=true +ReadWritePaths=/opt/sub2api + +# Environment - Server configuration +# Modify these values to change listen address and port +Environment=GIN_MODE=release +Environment=SERVER_HOST=0.0.0.0 +Environment=SERVER_PORT=8080 + +[Install] +WantedBy=multi-user.target diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 00000000..f1e43ef8 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Sub2API - AI API Gateway + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 00000000..61d40f22 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2932 @@ +{ + "name": "sub2api-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sub2api-frontend", + "version": "1.0.0", + "dependencies": { + "@vueuse/core": "^10.7.0", + "axios": "^1.6.2", + "chart.js": "^4.4.1", + "pinia": "^2.1.7", + "vue": "^3.4.0", + "vue-chartjs": "^5.3.0", + "vue-i18n": "^9.14.5", + "vue-router": "^4.2.5" + }, + "devDependencies": { + "@types/node": "^20.10.5", + "@vitejs/plugin-vue": "^4.5.2", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.32", + "tailwindcss": "^3.4.0", + "typescript": "^5.3.3", + "vite": "^5.0.10", + "vue-tsc": "^1.8.25" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@intlify/core-base": { + "version": "9.14.5", + "resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-9.14.5.tgz", + "integrity": "sha512-5ah5FqZG4pOoHjkvs8mjtv+gPKYU0zCISaYNjBNNqYiaITxW8ZtVih3GS/oTOqN8d9/mDLyrjD46GBApNxmlsA==", + "license": "MIT", + "dependencies": { + "@intlify/message-compiler": "9.14.5", + "@intlify/shared": "9.14.5" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/message-compiler": { + "version": "9.14.5", + "resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-9.14.5.tgz", + "integrity": "sha512-IHzgEu61/YIpQV5Pc3aRWScDcnFKWvQA9kigcINcCBXN8mbW+vk9SK+lDxA6STzKQsVJxUPg9ACC52pKKo3SVQ==", + "license": "MIT", + "dependencies": { + "@intlify/shared": "9.14.5", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/shared": { + "version": "9.14.5", + "resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-9.14.5.tgz", + "integrity": "sha512-9gB+E53BYuAEMhbCAxVgG38EZrk59sxBtv3jSizNL2hEWlgjBjAw1AwpLHtNaeda12pe6W20OGEa0TwuMSRbyQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmmirror.com/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.26", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.26.tgz", + "integrity": "sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz", + "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0 || ^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-1.11.1.tgz", + "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "1.11.1" + } + }, + "node_modules/@volar/source-map": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-1.11.1.tgz", + "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "muggle-string": "^0.3.1" + } + }, + "node_modules/@volar/typescript": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-1.11.1.tgz", + "integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "1.11.1", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.25.tgz", + "integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.25", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.25.tgz", + "integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.25.tgz", + "integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.25", + "@vue/compiler-dom": "3.5.25", + "@vue/compiler-ssr": "3.5.25", + "@vue/shared": "3.5.25", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.25.tgz", + "integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "1.8.27", + "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-1.8.27.tgz", + "integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "~1.11.1", + "@volar/source-map": "~1.11.1", + "@vue/compiler-dom": "^3.3.0", + "@vue/shared": "^3.3.0", + "computeds": "^0.0.1", + "minimatch": "^9.0.3", + "muggle-string": "^0.3.1", + "path-browserify": "^1.0.1", + "vue-template-compiler": "^2.7.14" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.25.tgz", + "integrity": "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.25.tgz", + "integrity": "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.25", + "@vue/shared": "3.5.25" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.25.tgz", + "integrity": "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.25", + "@vue/runtime-core": "3.5.25", + "@vue/shared": "3.5.25", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.25.tgz", + "integrity": "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.25", + "@vue/shared": "3.5.25" + }, + "peerDependencies": { + "vue": "3.5.25" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.25.tgz", + "integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.22", + "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.7", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.7.tgz", + "integrity": "sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001760", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmmirror.com/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/computeds": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/computeds/-/computeds-0.0.1.tgz", + "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/muggle-string": { + "version": "0.3.1", + "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.3.1.tgz", + "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmmirror.com/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.25.tgz", + "integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.25", + "@vue/compiler-sfc": "3.5.25", + "@vue/runtime-dom": "3.5.25", + "@vue/server-renderer": "3.5.25", + "@vue/shared": "3.5.25" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-chartjs": { + "version": "5.3.3", + "resolved": "https://registry.npmmirror.com/vue-chartjs/-/vue-chartjs-5.3.3.tgz", + "integrity": "sha512-jqxtL8KZ6YJ5NTv6XzrzLS7osyegOi28UGNZW0h9OkDL7Sh1396ht4Dorh04aKrl2LiSalQ84WtqiG0RIJb0tA==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "vue": "^3.0.0-0 || ^2.7.0" + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-i18n": { + "version": "9.14.5", + "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-9.14.5.tgz", + "integrity": "sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g==", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "9.14.5", + "@intlify/shared": "9.14.5", + "@vue/devtools-api": "^6.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-template-compiler": { + "version": "2.7.16", + "resolved": "https://registry.npmmirror.com/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "1.8.27", + "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-1.8.27.tgz", + "integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "~1.11.1", + "@vue/language-core": "1.8.27", + "semver": "^7.5.4" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": "*" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..028ff591 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,32 @@ +{ + "name": "sub2api-frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc && vite build", + "preview": "vite preview", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix" + }, + "dependencies": { + "@vueuse/core": "^10.7.0", + "axios": "^1.6.2", + "chart.js": "^4.4.1", + "pinia": "^2.1.7", + "vue": "^3.4.0", + "vue-chartjs": "^5.3.0", + "vue-i18n": "^9.14.5", + "vue-router": "^4.2.5" + }, + "devDependencies": { + "@types/node": "^20.10.5", + "@vitejs/plugin-vue": "^4.5.2", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.32", + "tailwindcss": "^3.4.0", + "typescript": "^5.3.3", + "vite": "^5.0.10", + "vue-tsc": "^1.8.25" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 00000000..2e7af2b7 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/public/logo.png b/frontend/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7d3fa420386bcf0e080278bc3c4ba25cb4c620fc GIT binary patch literal 149928 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�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

M* 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

0Tf99VT&e` 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 P00000NkvXXu0mjf$Mz76 literal 0 HcmV?d00001 diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 00000000..0decfba9 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,60 @@ + + + diff --git a/frontend/src/api/admin/accounts.ts b/frontend/src/api/admin/accounts.ts new file mode 100644 index 00000000..bcb1d40e --- /dev/null +++ b/frontend/src/api/admin/accounts.ts @@ -0,0 +1,270 @@ +/** + * Admin Accounts API endpoints + * Handles AI platform account management for administrators + */ + +import { apiClient } from '../client'; +import type { + Account, + CreateAccountRequest, + UpdateAccountRequest, + PaginatedResponse, + AccountUsageInfo, + WindowStats, +} from '@/types'; + +/** + * List all accounts with pagination + * @param page - Page number (default: 1) + * @param pageSize - Items per page (default: 20) + * @param filters - Optional filters + * @returns Paginated list of accounts + */ +export async function list( + page: number = 1, + pageSize: number = 20, + filters?: { + platform?: string; + type?: string; + status?: string; + search?: string; + } +): Promise> { + const { data } = await apiClient.get>('/admin/accounts', { + params: { + page, + page_size: pageSize, + ...filters, + }, + }); + return data; +} + +/** + * Get account by ID + * @param id - Account ID + * @returns Account details + */ +export async function getById(id: number): Promise { + const { data } = await apiClient.get(`/admin/accounts/${id}`); + return data; +} + +/** + * Create new account + * @param accountData - Account data + * @returns Created account + */ +export async function create(accountData: CreateAccountRequest): Promise { + const { data } = await apiClient.post('/admin/accounts', accountData); + return data; +} + +/** + * Update account + * @param id - Account ID + * @param updates - Fields to update + * @returns Updated account + */ +export async function update(id: number, updates: UpdateAccountRequest): Promise { + const { data } = await apiClient.put(`/admin/accounts/${id}`, updates); + return data; +} + +/** + * Delete account + * @param id - Account ID + * @returns Success confirmation + */ +export async function deleteAccount(id: number): Promise<{ message: string }> { + const { data } = await apiClient.delete<{ message: string }>(`/admin/accounts/${id}`); + return data; +} + +/** + * Toggle account status + * @param id - Account ID + * @param status - New status + * @returns Updated account + */ +export async function toggleStatus( + id: number, + status: 'active' | 'inactive' +): Promise { + return update(id, { status }); +} + +/** + * Test account connectivity + * @param id - Account ID + * @returns Test result + */ +export async function testAccount(id: number): Promise<{ + success: boolean; + message: string; + latency_ms?: number; +}> { + const { data } = await apiClient.post<{ + success: boolean; + message: string; + latency_ms?: number; + }>(`/admin/accounts/${id}/test`); + return data; +} + +/** + * Refresh account credentials + * @param id - Account ID + * @returns Updated account + */ +export async function refreshCredentials(id: number): Promise { + const { data } = await apiClient.post(`/admin/accounts/${id}/refresh`); + return data; +} + +/** + * Get account usage statistics + * @param id - Account ID + * @param period - Time period + * @returns Account usage statistics + */ +export async function getStats( + id: number, + period: string = 'month' +): Promise<{ + total_requests: number; + successful_requests: number; + failed_requests: number; + total_tokens: number; + average_response_time: number; +}> { + const { data } = await apiClient.get<{ + total_requests: number; + successful_requests: number; + failed_requests: number; + total_tokens: number; + average_response_time: number; + }>(`/admin/accounts/${id}/stats`, { + params: { period }, + }); + return data; +} + +/** + * Clear account error + * @param id - Account ID + * @returns Updated account + */ +export async function clearError(id: number): Promise { + const { data } = await apiClient.post(`/admin/accounts/${id}/clear-error`); + return data; +} + +/** + * Get account usage information (5h/7d window) + * @param id - Account ID + * @returns Account usage info + */ +export async function getUsage(id: number): Promise { + const { data } = await apiClient.get(`/admin/accounts/${id}/usage`); + return data; +} + +/** + * Clear account rate limit status + * @param id - Account ID + * @returns Success confirmation + */ +export async function clearRateLimit(id: number): Promise<{ message: string }> { + const { data } = await apiClient.post<{ message: string }>(`/admin/accounts/${id}/clear-rate-limit`); + return data; +} + +/** + * Generate OAuth authorization URL + * @param endpoint - API endpoint path + * @param config - Proxy configuration + * @returns Auth URL and session ID + */ +export async function generateAuthUrl( + endpoint: string, + config: { proxy_id?: number } +): Promise<{ auth_url: string; session_id: string }> { + const { data } = await apiClient.post<{ auth_url: string; session_id: string }>(endpoint, config); + return data; +} + +/** + * Exchange authorization code for tokens + * @param endpoint - API endpoint path + * @param exchangeData - Session ID, code, and optional proxy config + * @returns Token information + */ +export async function exchangeCode( + endpoint: string, + exchangeData: { session_id: string; code: string; proxy_id?: number } +): Promise> { + const { data } = await apiClient.post>(endpoint, exchangeData); + return data; +} + +/** + * Batch create accounts + * @param accounts - Array of account data + * @returns Results of batch creation + */ +export async function batchCreate(accounts: CreateAccountRequest[]): Promise<{ + success: number; + failed: number; + results: Array<{ success: boolean; account?: Account; error?: string }>; +}> { + const { data } = await apiClient.post<{ + success: number; + failed: number; + results: Array<{ success: boolean; account?: Account; error?: string }>; + }>('/admin/accounts/batch', { accounts }); + return data; +} + +/** + * Get account today statistics + * @param id - Account ID + * @returns Today's stats (requests, tokens, cost) + */ +export async function getTodayStats(id: number): Promise { + const { data } = await apiClient.get(`/admin/accounts/${id}/today-stats`); + return data; +} + +/** + * Set account schedulable status + * @param id - Account ID + * @param schedulable - Whether the account should participate in scheduling + * @returns Updated account + */ +export async function setSchedulable(id: number, schedulable: boolean): Promise { + const { data } = await apiClient.post(`/admin/accounts/${id}/schedulable`, { schedulable }); + return data; +} + +export const accountsAPI = { + list, + getById, + create, + update, + delete: deleteAccount, + toggleStatus, + testAccount, + refreshCredentials, + getStats, + clearError, + getUsage, + getTodayStats, + clearRateLimit, + setSchedulable, + generateAuthUrl, + exchangeCode, + batchCreate, +}; + +export default accountsAPI; diff --git a/frontend/src/api/admin/dashboard.ts b/frontend/src/api/admin/dashboard.ts new file mode 100644 index 00000000..36f4ee68 --- /dev/null +++ b/frontend/src/api/admin/dashboard.ts @@ -0,0 +1,173 @@ +/** + * Admin Dashboard API endpoints + * Provides system-wide statistics and metrics + */ + +import { apiClient } from '../client'; +import type { DashboardStats, TrendDataPoint, ModelStat, ApiKeyUsageTrendPoint, UserUsageTrendPoint } from '@/types'; + +/** + * Get dashboard statistics + * @returns Dashboard statistics including users, keys, accounts, and token usage + */ +export async function getStats(): Promise { + const { data } = await apiClient.get('/admin/dashboard/stats'); + return data; +} + +/** + * Get real-time metrics + * @returns Real-time system metrics + */ +export async function getRealtimeMetrics(): Promise<{ + active_requests: number; + requests_per_minute: number; + average_response_time: number; + error_rate: number; +}> { + const { data } = await apiClient.get<{ + active_requests: number; + requests_per_minute: number; + average_response_time: number; + error_rate: number; + }>('/admin/dashboard/realtime'); + return data; +} + +export interface TrendParams { + start_date?: string; + end_date?: string; + granularity?: 'day' | 'hour'; +} + +export interface TrendResponse { + trend: TrendDataPoint[]; + start_date: string; + end_date: string; + granularity: string; +} + +/** + * Get usage trend data + * @param params - Query parameters for filtering + * @returns Usage trend data + */ +export async function getUsageTrend(params?: TrendParams): Promise { + const { data } = await apiClient.get('/admin/dashboard/trend', { params }); + return data; +} + +export interface ModelStatsResponse { + models: ModelStat[]; + start_date: string; + end_date: string; +} + +/** + * Get model usage statistics + * @param params - Query parameters for filtering + * @returns Model usage statistics + */ +export async function getModelStats(params?: { start_date?: string; end_date?: string }): Promise { + const { data } = await apiClient.get('/admin/dashboard/models', { params }); + return data; +} + +export interface ApiKeyTrendParams extends TrendParams { + limit?: number; +} + +export interface ApiKeyTrendResponse { + trend: ApiKeyUsageTrendPoint[]; + start_date: string; + end_date: string; + granularity: string; +} + +/** + * Get API key usage trend data + * @param params - Query parameters for filtering + * @returns API key usage trend data + */ +export async function getApiKeyUsageTrend(params?: ApiKeyTrendParams): Promise { + const { data } = await apiClient.get('/admin/dashboard/api-keys-trend', { params }); + return data; +} + +export interface UserTrendParams extends TrendParams { + limit?: number; +} + +export interface UserTrendResponse { + trend: UserUsageTrendPoint[]; + start_date: string; + end_date: string; + granularity: string; +} + +/** + * Get user usage trend data + * @param params - Query parameters for filtering + * @returns User usage trend data + */ +export async function getUserUsageTrend(params?: UserTrendParams): Promise { + const { data } = await apiClient.get('/admin/dashboard/users-trend', { params }); + return data; +} + +export interface BatchUserUsageStats { + user_id: number; + today_actual_cost: number; + total_actual_cost: number; +} + +export interface BatchUsersUsageResponse { + stats: Record; +} + +/** + * Get batch usage stats for multiple users + * @param userIds - Array of user IDs + * @returns Usage stats map keyed by user ID + */ +export async function getBatchUsersUsage(userIds: number[]): Promise { + const { data } = await apiClient.post('/admin/dashboard/users-usage', { + user_ids: userIds, + }); + return data; +} + +export interface BatchApiKeyUsageStats { + api_key_id: number; + today_actual_cost: number; + total_actual_cost: number; +} + +export interface BatchApiKeysUsageResponse { + stats: Record; +} + +/** + * Get batch usage stats for multiple API keys + * @param apiKeyIds - Array of API key IDs + * @returns Usage stats map keyed by API key ID + */ +export async function getBatchApiKeysUsage(apiKeyIds: number[]): Promise { + const { data } = await apiClient.post('/admin/dashboard/api-keys-usage', { + api_key_ids: apiKeyIds, + }); + return data; +} + +export const dashboardAPI = { + getStats, + getRealtimeMetrics, + getUsageTrend, + getModelStats, + getApiKeyUsageTrend, + getUserUsageTrend, + getBatchUsersUsage, + getBatchApiKeysUsage, +}; + +export default dashboardAPI; diff --git a/frontend/src/api/admin/groups.ts b/frontend/src/api/admin/groups.ts new file mode 100644 index 00000000..1bc530ec --- /dev/null +++ b/frontend/src/api/admin/groups.ts @@ -0,0 +1,170 @@ +/** + * Admin Groups API endpoints + * Handles API key group management for administrators + */ + +import { apiClient } from '../client'; +import type { + Group, + GroupPlatform, + CreateGroupRequest, + UpdateGroupRequest, + PaginatedResponse, +} from '@/types'; + +/** + * List all groups with pagination + * @param page - Page number (default: 1) + * @param pageSize - Items per page (default: 20) + * @param filters - Optional filters (platform, status, is_exclusive) + * @returns Paginated list of groups + */ +export async function list( + page: number = 1, + pageSize: number = 20, + filters?: { + platform?: GroupPlatform; + status?: 'active' | 'inactive'; + is_exclusive?: boolean; + } +): Promise> { + const { data } = await apiClient.get>('/admin/groups', { + params: { + page, + page_size: pageSize, + ...filters, + }, + }); + return data; +} + +/** + * Get all active groups (without pagination) + * @param platform - Optional platform filter + * @returns List of all active groups + */ +export async function getAll(platform?: GroupPlatform): Promise { + const { data } = await apiClient.get('/admin/groups/all', { + params: platform ? { platform } : undefined + }); + return data; +} + +/** + * Get active groups by platform + * @param platform - Platform to filter by + * @returns List of groups for the specified platform + */ +export async function getByPlatform(platform: GroupPlatform): Promise { + return getAll(platform); +} + +/** + * Get group by ID + * @param id - Group ID + * @returns Group details + */ +export async function getById(id: number): Promise { + const { data } = await apiClient.get(`/admin/groups/${id}`); + return data; +} + +/** + * Create new group + * @param groupData - Group data + * @returns Created group + */ +export async function create(groupData: CreateGroupRequest): Promise { + const { data } = await apiClient.post('/admin/groups', groupData); + return data; +} + +/** + * Update group + * @param id - Group ID + * @param updates - Fields to update + * @returns Updated group + */ +export async function update(id: number, updates: UpdateGroupRequest): Promise { + const { data } = await apiClient.put(`/admin/groups/${id}`, updates); + return data; +} + +/** + * Delete group + * @param id - Group ID + * @returns Success confirmation + */ +export async function deleteGroup(id: number): Promise<{ message: string }> { + const { data } = await apiClient.delete<{ message: string }>(`/admin/groups/${id}`); + return data; +} + +/** + * Toggle group status + * @param id - Group ID + * @param status - New status + * @returns Updated group + */ +export async function toggleStatus( + id: number, + status: 'active' | 'inactive' +): Promise { + return update(id, { status }); +} + +/** + * Get group statistics + * @param id - Group ID + * @returns Group usage statistics + */ +export async function getStats(id: number): Promise<{ + total_api_keys: number; + active_api_keys: number; + total_requests: number; + total_cost: number; +}> { + const { data } = await apiClient.get<{ + total_api_keys: number; + active_api_keys: number; + total_requests: number; + total_cost: number; + }>(`/admin/groups/${id}/stats`); + return data; +} + +/** + * Get API keys in a group + * @param id - Group ID + * @param page - Page number + * @param pageSize - Items per page + * @returns Paginated list of API keys in the group + */ +export async function getGroupApiKeys( + id: number, + page: number = 1, + pageSize: number = 20 +): Promise> { + const { data } = await apiClient.get>( + `/admin/groups/${id}/api-keys`, + { + params: { page, page_size: pageSize }, + } + ); + return data; +} + +export const groupsAPI = { + list, + getAll, + getByPlatform, + getById, + create, + update, + delete: deleteGroup, + toggleStatus, + getStats, + getGroupApiKeys, +}; + +export default groupsAPI; diff --git a/frontend/src/api/admin/index.ts b/frontend/src/api/admin/index.ts new file mode 100644 index 00000000..4c2baea9 --- /dev/null +++ b/frontend/src/api/admin/index.ts @@ -0,0 +1,35 @@ +/** + * Admin API barrel export + * Centralized exports for all admin API modules + */ + +import dashboardAPI from './dashboard'; +import usersAPI from './users'; +import groupsAPI from './groups'; +import accountsAPI from './accounts'; +import proxiesAPI from './proxies'; +import redeemAPI from './redeem'; +import settingsAPI from './settings'; +import systemAPI from './system'; +import subscriptionsAPI from './subscriptions'; +import usageAPI from './usage'; + +/** + * Unified admin API object for convenient access + */ +export const adminAPI = { + dashboard: dashboardAPI, + users: usersAPI, + groups: groupsAPI, + accounts: accountsAPI, + proxies: proxiesAPI, + redeem: redeemAPI, + settings: settingsAPI, + system: systemAPI, + subscriptions: subscriptionsAPI, + usage: usageAPI, +}; + +export { dashboardAPI, usersAPI, groupsAPI, accountsAPI, proxiesAPI, redeemAPI, settingsAPI, systemAPI, subscriptionsAPI, usageAPI }; + +export default adminAPI; diff --git a/frontend/src/api/admin/proxies.ts b/frontend/src/api/admin/proxies.ts new file mode 100644 index 00000000..0ec092a7 --- /dev/null +++ b/frontend/src/api/admin/proxies.ts @@ -0,0 +1,211 @@ +/** + * Admin Proxies API endpoints + * Handles proxy server management for administrators + */ + +import { apiClient } from '../client'; +import type { + Proxy, + CreateProxyRequest, + UpdateProxyRequest, + PaginatedResponse, +} from '@/types'; + +/** + * List all proxies with pagination + * @param page - Page number (default: 1) + * @param pageSize - Items per page (default: 20) + * @param filters - Optional filters + * @returns Paginated list of proxies + */ +export async function list( + page: number = 1, + pageSize: number = 20, + filters?: { + protocol?: string; + status?: 'active' | 'inactive'; + search?: string; + } +): Promise> { + const { data } = await apiClient.get>('/admin/proxies', { + params: { + page, + page_size: pageSize, + ...filters, + }, + }); + return data; +} + +/** + * Get all active proxies (without pagination) + * @returns List of all active proxies + */ +export async function getAll(): Promise { + const { data } = await apiClient.get('/admin/proxies/all'); + return data; +} + +/** + * Get all active proxies with account count (sorted by creation time desc) + * @returns List of all active proxies with account count + */ +export async function getAllWithCount(): Promise { + const { data } = await apiClient.get('/admin/proxies/all', { + params: { with_count: 'true' }, + }); + return data; +} + +/** + * Get proxy by ID + * @param id - Proxy ID + * @returns Proxy details + */ +export async function getById(id: number): Promise { + const { data } = await apiClient.get(`/admin/proxies/${id}`); + return data; +} + +/** + * Create new proxy + * @param proxyData - Proxy data + * @returns Created proxy + */ +export async function create(proxyData: CreateProxyRequest): Promise { + const { data } = await apiClient.post('/admin/proxies', proxyData); + return data; +} + +/** + * Update proxy + * @param id - Proxy ID + * @param updates - Fields to update + * @returns Updated proxy + */ +export async function update(id: number, updates: UpdateProxyRequest): Promise { + const { data } = await apiClient.put(`/admin/proxies/${id}`, updates); + return data; +} + +/** + * Delete proxy + * @param id - Proxy ID + * @returns Success confirmation + */ +export async function deleteProxy(id: number): Promise<{ message: string }> { + const { data } = await apiClient.delete<{ message: string }>(`/admin/proxies/${id}`); + return data; +} + +/** + * Toggle proxy status + * @param id - Proxy ID + * @param status - New status + * @returns Updated proxy + */ +export async function toggleStatus( + id: number, + status: 'active' | 'inactive' +): Promise { + return update(id, { status }); +} + +/** + * Test proxy connectivity + * @param id - Proxy ID + * @returns Test result with IP info + */ +export async function testProxy(id: number): Promise<{ + success: boolean; + message: string; + latency_ms?: number; + ip_address?: string; + city?: string; + region?: string; + country?: string; +}> { + const { data } = await apiClient.post<{ + success: boolean; + message: string; + latency_ms?: number; + ip_address?: string; + city?: string; + region?: string; + country?: string; + }>(`/admin/proxies/${id}/test`); + return data; +} + +/** + * Get proxy usage statistics + * @param id - Proxy ID + * @returns Proxy usage statistics + */ +export async function getStats(id: number): Promise<{ + total_accounts: number; + active_accounts: number; + total_requests: number; + success_rate: number; + average_latency: number; +}> { + const { data } = await apiClient.get<{ + total_accounts: number; + active_accounts: number; + total_requests: number; + success_rate: number; + average_latency: number; + }>(`/admin/proxies/${id}/stats`); + return data; +} + +/** + * Get accounts using a proxy + * @param id - Proxy ID + * @returns List of accounts using the proxy + */ +export async function getProxyAccounts(id: number): Promise> { + const { data } = await apiClient.get>( + `/admin/proxies/${id}/accounts` + ); + return data; +} + +/** + * Batch create proxies + * @param proxies - Array of proxy data to create + * @returns Creation result with count of created and skipped + */ +export async function batchCreate(proxies: Array<{ + protocol: string; + host: string; + port: number; + username?: string; + password?: string; +}>): Promise<{ + created: number; + skipped: number; +}> { + const { data } = await apiClient.post<{ + created: number; + skipped: number; + }>('/admin/proxies/batch', { proxies }); + return data; +} + +export const proxiesAPI = { + list, + getAll, + getAllWithCount, + getById, + create, + update, + delete: deleteProxy, + toggleStatus, + testProxy, + getStats, + getProxyAccounts, + batchCreate, +}; + +export default proxiesAPI; diff --git a/frontend/src/api/admin/redeem.ts b/frontend/src/api/admin/redeem.ts new file mode 100644 index 00000000..815019ce --- /dev/null +++ b/frontend/src/api/admin/redeem.ts @@ -0,0 +1,170 @@ +/** + * Admin Redeem Codes API endpoints + * Handles redeem code generation and management for administrators + */ + +import { apiClient } from '../client'; +import type { + RedeemCode, + GenerateRedeemCodesRequest, + RedeemCodeType, + PaginatedResponse, +} from '@/types'; + +/** + * List all redeem codes with pagination + * @param page - Page number (default: 1) + * @param pageSize - Items per page (default: 20) + * @param filters - Optional filters + * @returns Paginated list of redeem codes + */ +export async function list( + page: number = 1, + pageSize: number = 20, + filters?: { + type?: RedeemCodeType; + status?: 'active' | 'used' | 'expired'; + search?: string; + } +): Promise> { + const { data } = await apiClient.get>('/admin/redeem-codes', { + params: { + page, + page_size: pageSize, + ...filters, + }, + }); + return data; +} + +/** + * Get redeem code by ID + * @param id - Redeem code ID + * @returns Redeem code details + */ +export async function getById(id: number): Promise { + const { data } = await apiClient.get(`/admin/redeem-codes/${id}`); + return data; +} + +/** + * Generate new redeem codes + * @param count - Number of codes to generate + * @param type - Type of redeem code + * @param value - Value of the code + * @param groupId - Group ID (required for subscription type) + * @param validityDays - Validity days (for subscription type) + * @returns Array of generated redeem codes + */ +export async function generate( + count: number, + type: RedeemCodeType, + value: number, + groupId?: number | null, + validityDays?: number +): Promise { + const payload: GenerateRedeemCodesRequest = { + count, + type, + value, + }; + + // 订阅类型专用字段 + if (type === 'subscription') { + payload.group_id = groupId; + if (validityDays && validityDays > 0) { + payload.validity_days = validityDays; + } + } + + const { data } = await apiClient.post('/admin/redeem-codes/generate', payload); + return data; +} + +/** + * Delete redeem code + * @param id - Redeem code ID + * @returns Success confirmation + */ +export async function deleteCode(id: number): Promise<{ message: string }> { + const { data } = await apiClient.delete<{ message: string }>(`/admin/redeem-codes/${id}`); + return data; +} + +/** + * Batch delete redeem codes + * @param ids - Array of redeem code IDs + * @returns Success confirmation + */ +export async function batchDelete(ids: number[]): Promise<{ + deleted: number; + message: string; +}> { + const { data } = await apiClient.post<{ + deleted: number; + message: string; + }>('/admin/redeem-codes/batch-delete', { ids }); + return data; +} + +/** + * Expire redeem code + * @param id - Redeem code ID + * @returns Updated redeem code + */ +export async function expire(id: number): Promise { + const { data } = await apiClient.post(`/admin/redeem-codes/${id}/expire`); + return data; +} + +/** + * Get redeem code statistics + * @returns Statistics about redeem codes + */ +export async function getStats(): Promise<{ + total_codes: number; + active_codes: number; + used_codes: number; + expired_codes: number; + total_value_distributed: number; + by_type: Record; +}> { + const { data } = await apiClient.get<{ + total_codes: number; + active_codes: number; + used_codes: number; + expired_codes: number; + total_value_distributed: number; + by_type: Record; + }>('/admin/redeem-codes/stats'); + return data; +} + +/** + * Export redeem codes to CSV + * @param filters - Optional filters + * @returns CSV data as blob + */ +export async function exportCodes(filters?: { + type?: RedeemCodeType; + status?: 'active' | 'used' | 'expired'; +}): Promise { + const response = await apiClient.get('/admin/redeem-codes/export', { + params: filters, + responseType: 'blob', + }); + return response.data; +} + +export const redeemAPI = { + list, + getById, + generate, + delete: deleteCode, + batchDelete, + expire, + getStats, + exportCodes, +}; + +export default redeemAPI; diff --git a/frontend/src/api/admin/settings.ts b/frontend/src/api/admin/settings.ts new file mode 100644 index 00000000..b33944d1 --- /dev/null +++ b/frontend/src/api/admin/settings.ts @@ -0,0 +1,109 @@ +/** + * Admin Settings API endpoints + * Handles system settings management for administrators + */ + +import { apiClient } from '../client'; + +/** + * System settings interface + */ +export interface SystemSettings { + // Registration settings + registration_enabled: boolean; + email_verify_enabled: boolean; + // Default settings + default_balance: number; + default_concurrency: number; + // OEM settings + site_name: string; + site_logo: string; + site_subtitle: string; + api_base_url: string; + contact_info: string; + // SMTP settings + smtp_host: string; + smtp_port: number; + smtp_username: string; + smtp_password: string; + smtp_from_email: string; + smtp_from_name: string; + smtp_use_tls: boolean; + // Cloudflare Turnstile settings + turnstile_enabled: boolean; + turnstile_site_key: string; + turnstile_secret_key: string; +} + +/** + * Get all system settings + * @returns System settings + */ +export async function getSettings(): Promise { + const { data } = await apiClient.get('/admin/settings'); + return data; +} + +/** + * Update system settings + * @param settings - Partial settings to update + * @returns Updated settings + */ +export async function updateSettings(settings: Partial): Promise { + const { data } = await apiClient.put('/admin/settings', settings); + return data; +} + +/** + * Test SMTP connection request + */ +export interface TestSmtpRequest { + smtp_host: string; + smtp_port: number; + smtp_username: string; + smtp_password: string; + smtp_use_tls: boolean; +} + +/** + * Test SMTP connection with provided config + * @param config - SMTP configuration to test + * @returns Test result message + */ +export async function testSmtpConnection(config: TestSmtpRequest): Promise<{ message: string }> { + const { data } = await apiClient.post<{ message: string }>('/admin/settings/test-smtp', config); + return data; +} + +/** + * Send test email request + */ +export interface SendTestEmailRequest { + email: string; + smtp_host: string; + smtp_port: number; + smtp_username: string; + smtp_password: string; + smtp_from_email: string; + smtp_from_name: string; + smtp_use_tls: boolean; +} + +/** + * Send test email with provided SMTP config + * @param request - Email address and SMTP config + * @returns Test result message + */ +export async function sendTestEmail(request: SendTestEmailRequest): Promise<{ message: string }> { + const { data } = await apiClient.post<{ message: string }>('/admin/settings/send-test-email', request); + return data; +} + +export const settingsAPI = { + getSettings, + updateSettings, + testSmtpConnection, + sendTestEmail, +}; + +export default settingsAPI; diff --git a/frontend/src/api/admin/subscriptions.ts b/frontend/src/api/admin/subscriptions.ts new file mode 100644 index 00000000..5d000b9a --- /dev/null +++ b/frontend/src/api/admin/subscriptions.ts @@ -0,0 +1,157 @@ +/** + * Admin Subscriptions API endpoints + * Handles user subscription management for administrators + */ + +import { apiClient } from '../client'; +import type { + UserSubscription, + SubscriptionProgress, + AssignSubscriptionRequest, + BulkAssignSubscriptionRequest, + ExtendSubscriptionRequest, + PaginatedResponse, +} from '@/types'; + +/** + * List all subscriptions with pagination + * @param page - Page number (default: 1) + * @param pageSize - Items per page (default: 20) + * @param filters - Optional filters (status, user_id, group_id) + * @returns Paginated list of subscriptions + */ +export async function list( + page: number = 1, + pageSize: number = 20, + filters?: { + status?: 'active' | 'expired' | 'revoked'; + user_id?: number; + group_id?: number; + } +): Promise> { + const { data } = await apiClient.get>('/admin/subscriptions', { + params: { + page, + page_size: pageSize, + ...filters, + }, + }); + return data; +} + +/** + * Get subscription by ID + * @param id - Subscription ID + * @returns Subscription details + */ +export async function getById(id: number): Promise { + const { data } = await apiClient.get(`/admin/subscriptions/${id}`); + return data; +} + +/** + * Get subscription progress + * @param id - Subscription ID + * @returns Subscription progress with usage stats + */ +export async function getProgress(id: number): Promise { + const { data } = await apiClient.get(`/admin/subscriptions/${id}/progress`); + return data; +} + +/** + * Assign subscription to user + * @param request - Assignment request + * @returns Created subscription + */ +export async function assign(request: AssignSubscriptionRequest): Promise { + const { data } = await apiClient.post('/admin/subscriptions/assign', request); + return data; +} + +/** + * Bulk assign subscriptions to multiple users + * @param request - Bulk assignment request + * @returns Created subscriptions + */ +export async function bulkAssign(request: BulkAssignSubscriptionRequest): Promise { + const { data } = await apiClient.post('/admin/subscriptions/bulk-assign', request); + return data; +} + +/** + * Extend subscription validity + * @param id - Subscription ID + * @param request - Extension request with days + * @returns Updated subscription + */ +export async function extend(id: number, request: ExtendSubscriptionRequest): Promise { + const { data } = await apiClient.post(`/admin/subscriptions/${id}/extend`, request); + return data; +} + +/** + * Revoke subscription + * @param id - Subscription ID + * @returns Success confirmation + */ +export async function revoke(id: number): Promise<{ message: string }> { + const { data } = await apiClient.delete<{ message: string }>(`/admin/subscriptions/${id}`); + return data; +} + +/** + * List subscriptions by group + * @param groupId - Group ID + * @param page - Page number + * @param pageSize - Items per page + * @returns Paginated list of subscriptions in the group + */ +export async function listByGroup( + groupId: number, + page: number = 1, + pageSize: number = 20 +): Promise> { + const { data } = await apiClient.get>( + `/admin/groups/${groupId}/subscriptions`, + { + params: { page, page_size: pageSize }, + } + ); + return data; +} + +/** + * List subscriptions by user + * @param userId - User ID + * @param page - Page number + * @param pageSize - Items per page + * @returns Paginated list of user's subscriptions + */ +export async function listByUser( + userId: number, + page: number = 1, + pageSize: number = 20 +): Promise> { + const { data } = await apiClient.get>( + `/admin/users/${userId}/subscriptions`, + { + params: { page, page_size: pageSize }, + } + ); + return data; +} + +export const subscriptionsAPI = { + list, + getById, + getProgress, + assign, + bulkAssign, + extend, + revoke, + listByGroup, + listByUser, +}; + +export default subscriptionsAPI; diff --git a/frontend/src/api/admin/system.ts b/frontend/src/api/admin/system.ts new file mode 100644 index 00000000..6f13856b --- /dev/null +++ b/frontend/src/api/admin/system.ts @@ -0,0 +1,48 @@ +/** + * System API endpoints for admin operations + */ + +import { apiClient } from '../client'; + +export interface ReleaseInfo { + name: string; + body: string; + published_at: string; + html_url: string; +} + +export interface VersionInfo { + current_version: string; + latest_version: string; + has_update: boolean; + release_info?: ReleaseInfo; + cached: boolean; + warning?: string; + build_type: string; // "source" for manual builds, "release" for CI builds +} + +/** + * Get current version + */ +export async function getVersion(): Promise<{ version: string }> { + const { data } = await apiClient.get<{ version: string }>('/admin/system/version'); + return data; +} + +/** + * Check for updates + * @param force - Force refresh from GitHub API + */ +export async function checkUpdates(force = false): Promise { + const { data } = await apiClient.get('/admin/system/check-updates', { + params: force ? { force: 'true' } : undefined, + }); + return data; +} + +export const systemAPI = { + getVersion, + checkUpdates, +}; + +export default systemAPI; diff --git a/frontend/src/api/admin/usage.ts b/frontend/src/api/admin/usage.ts new file mode 100644 index 00000000..2c5e800e --- /dev/null +++ b/frontend/src/api/admin/usage.ts @@ -0,0 +1,112 @@ +/** + * Admin Usage API endpoints + * Handles admin-level usage logs and statistics retrieval + */ + +import { apiClient } from '../client'; +import type { + UsageLog, + UsageQueryParams, + PaginatedResponse, +} from '@/types'; + +// ==================== Types ==================== + +export interface AdminUsageStatsResponse { + total_requests: number; + total_input_tokens: number; + total_output_tokens: number; + total_cache_tokens: number; + total_tokens: number; + total_cost: number; + total_actual_cost: number; + average_duration_ms: number; +} + +export interface SimpleUser { + id: number; + email: string; +} + +export interface SimpleApiKey { + id: number; + name: string; + user_id: number; +} + +export interface AdminUsageQueryParams extends UsageQueryParams { + user_id?: number; +} + +// ==================== API Functions ==================== + +/** + * List all usage logs with optional filters (admin only) + * @param params - Query parameters for filtering and pagination + * @returns Paginated list of usage logs + */ +export async function list(params: AdminUsageQueryParams): Promise> { + const { data } = await apiClient.get>('/admin/usage', { + params, + }); + return data; +} + +/** + * Get usage statistics with optional filters (admin only) + * @param params - Query parameters (user_id, api_key_id, period/date range) + * @returns Usage statistics + */ +export async function getStats(params: { + user_id?: number; + api_key_id?: number; + period?: string; + start_date?: string; + end_date?: string; +}): Promise { + const { data } = await apiClient.get('/admin/usage/stats', { + params, + }); + return data; +} + +/** + * Search users by email keyword (admin only) + * @param keyword - Email keyword to search + * @returns List of matching users (max 30) + */ +export async function searchUsers(keyword: string): Promise { + const { data } = await apiClient.get('/admin/usage/search-users', { + params: { q: keyword }, + }); + return data; +} + +/** + * Search API keys by user ID and/or keyword (admin only) + * @param userId - Optional user ID to filter by + * @param keyword - Optional keyword to search in key name + * @returns List of matching API keys (max 30) + */ +export async function searchApiKeys(userId?: number, keyword?: string): Promise { + const params: Record = {}; + if (userId !== undefined) { + params.user_id = userId; + } + if (keyword) { + params.q = keyword; + } + const { data } = await apiClient.get('/admin/usage/search-api-keys', { + params, + }); + return data; +} + +export const adminUsageAPI = { + list, + getStats, + searchUsers, + searchApiKeys, +}; + +export default adminUsageAPI; diff --git a/frontend/src/api/admin/users.ts b/frontend/src/api/admin/users.ts new file mode 100644 index 00000000..8eb11dc8 --- /dev/null +++ b/frontend/src/api/admin/users.ts @@ -0,0 +1,168 @@ +/** + * Admin Users API endpoints + * Handles user management for administrators + */ + +import { apiClient } from '../client'; +import type { User, UpdateUserRequest, PaginatedResponse } from '@/types'; + +/** + * List all users with pagination + * @param page - Page number (default: 1) + * @param pageSize - Items per page (default: 20) + * @param filters - Optional filters (status, role, search) + * @returns Paginated list of users + */ +export async function list( + page: number = 1, + pageSize: number = 20, + filters?: { + status?: 'active' | 'disabled'; + role?: 'admin' | 'user'; + search?: string; + } +): Promise> { + const { data } = await apiClient.get>('/admin/users', { + params: { + page, + page_size: pageSize, + ...filters, + }, + }); + return data; +} + +/** + * Get user by ID + * @param id - User ID + * @returns User details + */ +export async function getById(id: number): Promise { + const { data } = await apiClient.get(`/admin/users/${id}`); + return data; +} + +/** + * Create new user + * @param userData - User data (email, password, etc.) + * @returns Created user + */ +export async function create(userData: { + email: string; + password: string; + balance?: number; + concurrency?: number; + allowed_groups?: number[] | null; +}): Promise { + const { data } = await apiClient.post('/admin/users', userData); + return data; +} + +/** + * Update user + * @param id - User ID + * @param updates - Fields to update + * @returns Updated user + */ +export async function update(id: number, updates: UpdateUserRequest): Promise { + const { data } = await apiClient.put(`/admin/users/${id}`, updates); + return data; +} + +/** + * Delete user + * @param id - User ID + * @returns Success confirmation + */ +export async function deleteUser(id: number): Promise<{ message: string }> { + const { data } = await apiClient.delete<{ message: string }>(`/admin/users/${id}`); + return data; +} + +/** + * Update user balance + * @param id - User ID + * @param balance - New balance + * @param operation - Operation type ('set', 'add', 'subtract') + * @returns Updated user + */ +export async function updateBalance( + id: number, + balance: number, + operation: 'set' | 'add' | 'subtract' = 'set' +): Promise { + const { data } = await apiClient.post(`/admin/users/${id}/balance`, { + balance, + operation, + }); + return data; +} + +/** + * Update user concurrency + * @param id - User ID + * @param concurrency - New concurrency limit + * @returns Updated user + */ +export async function updateConcurrency(id: number, concurrency: number): Promise { + return update(id, { concurrency }); +} + +/** + * Toggle user status + * @param id - User ID + * @param status - New status + * @returns Updated user + */ +export async function toggleStatus(id: number, status: 'active' | 'disabled'): Promise { + return update(id, { status }); +} + +/** + * Get user's API keys + * @param id - User ID + * @returns List of user's API keys + */ +export async function getUserApiKeys(id: number): Promise> { + const { data } = await apiClient.get>(`/admin/users/${id}/api-keys`); + return data; +} + +/** + * Get user's usage statistics + * @param id - User ID + * @param period - Time period + * @returns User usage statistics + */ +export async function getUserUsageStats( + id: number, + period: string = 'month' +): Promise<{ + total_requests: number; + total_cost: number; + total_tokens: number; +}> { + const { data } = await apiClient.get<{ + total_requests: number; + total_cost: number; + total_tokens: number; + }>(`/admin/users/${id}/usage`, { + params: { period }, + }); + return data; +} + +export const usersAPI = { + list, + getById, + create, + update, + delete: deleteUser, + updateBalance, + updateConcurrency, + toggleStatus, + getUserApiKeys, + getUserUsageStats, +}; + +export default usersAPI; diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts new file mode 100644 index 00000000..9d8c1597 --- /dev/null +++ b/frontend/src/api/auth.ts @@ -0,0 +1,120 @@ +/** + * Authentication API endpoints + * Handles user login, registration, and logout operations + */ + +import { apiClient } from './client'; +import type { LoginRequest, RegisterRequest, AuthResponse, User, SendVerifyCodeRequest, SendVerifyCodeResponse, PublicSettings } from '@/types'; + +/** + * Store authentication token in localStorage + */ +export function setAuthToken(token: string): void { + localStorage.setItem('auth_token', token); +} + +/** + * Get authentication token from localStorage + */ +export function getAuthToken(): string | null { + return localStorage.getItem('auth_token'); +} + +/** + * Clear authentication token from localStorage + */ +export function clearAuthToken(): void { + localStorage.removeItem('auth_token'); + localStorage.removeItem('auth_user'); +} + +/** + * User login + * @param credentials - Username and password + * @returns Authentication response with token and user data + */ +export async function login(credentials: LoginRequest): Promise { + const { data } = await apiClient.post('/auth/login', credentials); + + // Store token and user data + setAuthToken(data.access_token); + localStorage.setItem('auth_user', JSON.stringify(data.user)); + + return data; +} + +/** + * User registration + * @param userData - Registration data (username, email, password) + * @returns Authentication response with token and user data + */ +export async function register(userData: RegisterRequest): Promise { + const { data } = await apiClient.post('/auth/register', userData); + + // Store token and user data + setAuthToken(data.access_token); + localStorage.setItem('auth_user', JSON.stringify(data.user)); + + return data; +} + +/** + * Get current authenticated user + * @returns User profile data + */ +export async function getCurrentUser(): Promise { + const { data } = await apiClient.get('/auth/me'); + return data; +} + +/** + * User logout + * Clears authentication token and user data from localStorage + */ +export function logout(): void { + clearAuthToken(); + // Optionally redirect to login page + // window.location.href = '/login'; +} + +/** + * Check if user is authenticated + * @returns True if user has valid token + */ +export function isAuthenticated(): boolean { + return getAuthToken() !== null; +} + +/** + * Get public settings (no auth required) + * @returns Public settings including registration and Turnstile config + */ +export async function getPublicSettings(): Promise { + const { data } = await apiClient.get('/settings/public'); + return data; +} + +/** + * Send verification code to email + * @param request - Email and optional Turnstile token + * @returns Response with countdown seconds + */ +export async function sendVerifyCode(request: SendVerifyCodeRequest): Promise { + const { data } = await apiClient.post('/auth/send-verify-code', request); + return data; +} + +export const authAPI = { + login, + register, + getCurrentUser, + logout, + isAuthenticated, + setAuthToken, + getAuthToken, + clearAuthToken, + getPublicSettings, + sendVerifyCode, +}; + +export default authAPI; diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 00000000..4847b22a --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,89 @@ +/** + * Axios HTTP Client Configuration + * Base client with interceptors for authentication and error handling + */ + +import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios'; +import type { ApiResponse } from '@/types'; + +// ==================== Axios Instance Configuration ==================== + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api/v1'; + +export const apiClient: AxiosInstance = axios.create({ + baseURL: API_BASE_URL, + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + }, +}); + +// ==================== Request Interceptor ==================== + +apiClient.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + // Attach token from localStorage + const token = localStorage.getItem('auth_token'); + if (token && config.headers) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +// ==================== Response Interceptor ==================== + +apiClient.interceptors.response.use( + (response) => { + // Unwrap standard API response format { code, message, data } + const apiResponse = response.data as ApiResponse; + if (apiResponse && typeof apiResponse === 'object' && 'code' in apiResponse) { + if (apiResponse.code === 0) { + // Success - return the data portion + response.data = apiResponse.data; + } else { + // API error + return Promise.reject({ + status: response.status, + code: apiResponse.code, + message: apiResponse.message || 'Unknown error', + }); + } + } + return response; + }, + (error: AxiosError>) => { + // Handle common errors + if (error.response) { + const { status, data } = error.response; + + // 401: Unauthorized - clear token and redirect to login + if (status === 401) { + localStorage.removeItem('auth_token'); + localStorage.removeItem('auth_user'); + // Only redirect if not already on login page + if (!window.location.pathname.includes('/login')) { + window.location.href = '/login'; + } + } + + // Return structured error + return Promise.reject({ + status, + code: data?.code, + message: data?.message || error.message, + }); + } + + // Network error + return Promise.reject({ + status: 0, + message: 'Network error. Please check your connection.', + }); + } +); + +export default apiClient; diff --git a/frontend/src/api/groups.ts b/frontend/src/api/groups.ts new file mode 100644 index 00000000..490c03cd --- /dev/null +++ b/frontend/src/api/groups.ts @@ -0,0 +1,25 @@ +/** + * User Groups API endpoints (non-admin) + * Handles group-related operations for regular users + */ + +import { apiClient } from './client'; +import type { Group } from '@/types'; + +/** + * Get available groups that the current user can bind to API keys + * This returns groups based on user's permissions: + * - Standard groups: public (non-exclusive) or explicitly allowed + * - Subscription groups: user has active subscription + * @returns List of available groups + */ +export async function getAvailable(): Promise { + const { data } = await apiClient.get('/groups/available'); + return data; +} + +export const userGroupsAPI = { + getAvailable, +}; + +export default userGroupsAPI; diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts new file mode 100644 index 00000000..fd3bc414 --- /dev/null +++ b/frontend/src/api/index.ts @@ -0,0 +1,23 @@ +/** + * API Client for Sub2API Backend + * Central export point for all API modules + */ + +// Re-export the HTTP client +export { apiClient } from './client'; + +// Auth API +export { authAPI } from './auth'; + +// User APIs +export { keysAPI } from './keys'; +export { usageAPI } from './usage'; +export { userAPI } from './user'; +export { redeemAPI, type RedeemHistoryItem } from './redeem'; +export { userGroupsAPI } from './groups'; + +// Admin APIs +export { adminAPI } from './admin'; + +// Default export +export { default } from './client'; diff --git a/frontend/src/api/keys.ts b/frontend/src/api/keys.ts new file mode 100644 index 00000000..754c06a0 --- /dev/null +++ b/frontend/src/api/keys.ts @@ -0,0 +1,100 @@ +/** + * API Keys management endpoints + * Handles CRUD operations for user API keys + */ + +import { apiClient } from './client'; +import type { + ApiKey, + CreateApiKeyRequest, + UpdateApiKeyRequest, + PaginatedResponse, +} from '@/types'; + +/** + * List all API keys for current user + * @param page - Page number (default: 1) + * @param pageSize - Items per page (default: 10) + * @returns Paginated list of API keys + */ +export async function list(page: number = 1, pageSize: number = 10): Promise> { + const { data } = await apiClient.get>('/keys', { + params: { page, page_size: pageSize }, + }); + return data; +} + +/** + * Get API key by ID + * @param id - API key ID + * @returns API key details + */ +export async function getById(id: number): Promise { + const { data } = await apiClient.get(`/keys/${id}`); + return data; +} + +/** + * Create new API key + * @param name - Key name + * @param groupId - Optional group ID + * @param customKey - Optional custom key value + * @returns Created API key + */ +export async function create(name: string, groupId?: number | null, customKey?: string): Promise { + const payload: CreateApiKeyRequest = { name }; + if (groupId !== undefined) { + payload.group_id = groupId; + } + if (customKey) { + payload.custom_key = customKey; + } + + const { data } = await apiClient.post('/keys', payload); + return data; +} + +/** + * Update API key + * @param id - API key ID + * @param updates - Fields to update + * @returns Updated API key + */ +export async function update(id: number, updates: UpdateApiKeyRequest): Promise { + const { data } = await apiClient.put(`/keys/${id}`, updates); + return data; +} + +/** + * Delete API key + * @param id - API key ID + * @returns Success confirmation + */ +export async function deleteKey(id: number): Promise<{ message: string }> { + const { data } = await apiClient.delete<{ message: string }>(`/keys/${id}`); + return data; +} + +/** + * Toggle API key status (active/inactive) + * @param id - API key ID + * @param status - New status + * @returns Updated API key + */ +export async function toggleStatus( + id: number, + status: 'active' | 'inactive' +): Promise { + return update(id, { status }); +} + +export const keysAPI = { + list, + getById, + create, + update, + delete: deleteKey, + toggleStatus, +}; + +export default keysAPI; diff --git a/frontend/src/api/redeem.ts b/frontend/src/api/redeem.ts new file mode 100644 index 00000000..2b127f1e --- /dev/null +++ b/frontend/src/api/redeem.ts @@ -0,0 +1,65 @@ +/** + * Redeem code API endpoints + * Handles redeem code redemption for users + */ + +import { apiClient } from './client'; +import type { RedeemCodeRequest } from '@/types'; + +export interface RedeemHistoryItem { + id: number; + code: string; + type: string; + value: number; + status: string; + used_at: string; + created_at: string; + // 订阅类型专用字段 + group_id?: number; + validity_days?: number; + group?: { + id: number; + name: string; + }; +} + +/** + * Redeem a code + * @param code - Redeem code string + * @returns Redemption result with updated balance or concurrency + */ +export async function redeem(code: string): Promise<{ + message: string; + type: string; + value: number; + new_balance?: number; + new_concurrency?: number; +}> { + const payload: RedeemCodeRequest = { code }; + + const { data } = await apiClient.post<{ + message: string; + type: string; + value: number; + new_balance?: number; + new_concurrency?: number; + }>('/redeem', payload); + + return data; +} + +/** + * Get user's redemption history + * @returns List of redeemed codes + */ +export async function getHistory(): Promise { + const { data } = await apiClient.get('/redeem/history'); + return data; +} + +export const redeemAPI = { + redeem, + getHistory, +}; + +export default redeemAPI; diff --git a/frontend/src/api/setup.ts b/frontend/src/api/setup.ts new file mode 100644 index 00000000..ecf7a84f --- /dev/null +++ b/frontend/src/api/setup.ts @@ -0,0 +1,87 @@ +/** + * Setup API endpoints + */ +import axios from 'axios'; + +// Create a separate client for setup endpoints (not under /api/v1) +const setupClient = axios.create({ + baseURL: '', + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + }, +}); + +export interface SetupStatus { + needs_setup: boolean; + step: string; +} + +export interface DatabaseConfig { + host: string; + port: number; + user: string; + password: string; + dbname: string; + sslmode: string; +} + +export interface RedisConfig { + host: string; + port: number; + password: string; + db: number; +} + +export interface AdminConfig { + email: string; + password: string; +} + +export interface ServerConfig { + host: string; + port: number; + mode: string; +} + +export interface InstallRequest { + database: DatabaseConfig; + redis: RedisConfig; + admin: AdminConfig; + server: ServerConfig; +} + +export interface InstallResponse { + message: string; + restart: boolean; +} + +/** + * Get setup status + */ +export async function getSetupStatus(): Promise { + const response = await setupClient.get('/setup/status'); + return response.data.data; +} + +/** + * Test database connection + */ +export async function testDatabase(config: DatabaseConfig): Promise { + await setupClient.post('/setup/test-db', config); +} + +/** + * Test Redis connection + */ +export async function testRedis(config: RedisConfig): Promise { + await setupClient.post('/setup/test-redis', config); +} + +/** + * Perform installation + */ +export async function install(config: InstallRequest): Promise { + const response = await setupClient.post('/setup/install', config); + return response.data.data; +} diff --git a/frontend/src/api/subscriptions.ts b/frontend/src/api/subscriptions.ts new file mode 100644 index 00000000..3725aa7a --- /dev/null +++ b/frontend/src/api/subscriptions.ts @@ -0,0 +1,72 @@ +/** + * User Subscription API + * API for regular users to view their own subscriptions and progress + */ + +import { apiClient } from './client'; +import type { UserSubscription, SubscriptionProgress } from '@/types'; + +/** + * Subscription summary for user dashboard + */ +export interface SubscriptionSummary { + active_count: number; + subscriptions: Array<{ + id: number; + group_name: string; + status: string; + daily_progress: number | null; + weekly_progress: number | null; + monthly_progress: number | null; + expires_at: string | null; + days_remaining: number | null; + }>; +} + +/** + * Get list of current user's subscriptions + */ +export async function getMySubscriptions(): Promise { + const response = await apiClient.get('/subscriptions'); + return response.data; +} + +/** + * Get current user's active subscriptions + */ +export async function getActiveSubscriptions(): Promise { + const response = await apiClient.get('/subscriptions/active'); + return response.data; +} + +/** + * Get progress for all user's active subscriptions + */ +export async function getSubscriptionsProgress(): Promise { + const response = await apiClient.get('/subscriptions/progress'); + return response.data; +} + +/** + * Get subscription summary for dashboard display + */ +export async function getSubscriptionSummary(): Promise { + const response = await apiClient.get('/subscriptions/summary'); + return response.data; +} + +/** + * Get progress for a specific subscription + */ +export async function getSubscriptionProgress(subscriptionId: number): Promise { + const response = await apiClient.get(`/subscriptions/${subscriptionId}/progress`); + return response.data; +} + +export default { + getMySubscriptions, + getActiveSubscriptions, + getSubscriptionsProgress, + getSubscriptionSummary, + getSubscriptionProgress, +}; diff --git a/frontend/src/api/usage.ts b/frontend/src/api/usage.ts new file mode 100644 index 00000000..7eafcd5b --- /dev/null +++ b/frontend/src/api/usage.ts @@ -0,0 +1,253 @@ +/** + * Usage tracking API endpoints + * Handles usage logs and statistics retrieval + */ + +import { apiClient } from './client'; +import type { + UsageLog, + UsageQueryParams, + UsageStatsResponse, + PaginatedResponse, + TrendDataPoint, + ModelStat, +} from '@/types'; + +// ==================== Dashboard Types ==================== + +export interface UserDashboardStats { + total_api_keys: number; + active_api_keys: number; + total_requests: number; + total_input_tokens: number; + total_output_tokens: number; + total_cache_creation_tokens: number; + total_cache_read_tokens: number; + total_tokens: number; + total_cost: number; // 标准计费 + total_actual_cost: number; // 实际扣除 + today_requests: number; + today_input_tokens: number; + today_output_tokens: number; + today_cache_creation_tokens: number; + today_cache_read_tokens: number; + today_tokens: number; + today_cost: number; // 今日标准计费 + today_actual_cost: number; // 今日实际扣除 + average_duration_ms: number; +} + +export interface TrendParams { + start_date?: string; + end_date?: string; + granularity?: 'day' | 'hour'; +} + +export interface TrendResponse { + trend: TrendDataPoint[]; + start_date: string; + end_date: string; + granularity: string; +} + +export interface ModelStatsResponse { + models: ModelStat[]; + start_date: string; + end_date: string; +} + +/** + * List usage logs with optional filters + * @param page - Page number (default: 1) + * @param pageSize - Items per page (default: 20) + * @param apiKeyId - Filter by API key ID + * @returns Paginated list of usage logs + */ +export async function list( + page: number = 1, + pageSize: number = 20, + apiKeyId?: number +): Promise> { + const params: UsageQueryParams = { + page, + page_size: pageSize, + }; + + if (apiKeyId !== undefined) { + params.api_key_id = apiKeyId; + } + + const { data } = await apiClient.get>('/usage', { + params, + }); + return data; +} + +/** + * Get usage logs with advanced query parameters + * @param params - Query parameters for filtering and pagination + * @returns Paginated list of usage logs + */ +export async function query(params: UsageQueryParams): Promise> { + const { data } = await apiClient.get>('/usage', { + params, + }); + return data; +} + +/** + * Get usage statistics for a specific period + * @param period - Time period ('today', 'week', 'month', 'year') + * @param apiKeyId - Optional API key ID filter + * @returns Usage statistics + */ +export async function getStats( + period: string = 'today', + apiKeyId?: number +): Promise { + const params: Record = { period }; + + if (apiKeyId !== undefined) { + params.api_key_id = apiKeyId; + } + + const { data } = await apiClient.get('/usage/stats', { + params, + }); + return data; +} + +/** + * Get usage statistics for a date range + * @param startDate - Start date (YYYY-MM-DD format) + * @param endDate - End date (YYYY-MM-DD format) + * @param apiKeyId - Optional API key ID filter + * @returns Usage statistics + */ +export async function getStatsByDateRange( + startDate: string, + endDate: string, + apiKeyId?: number +): Promise { + const params: Record = { + start_date: startDate, + end_date: endDate, + }; + + if (apiKeyId !== undefined) { + params.api_key_id = apiKeyId; + } + + const { data } = await apiClient.get('/usage/stats', { + params, + }); + return data; +} + +/** + * Get usage by date range + * @param startDate - Start date (ISO format) + * @param endDate - End date (ISO format) + * @param apiKeyId - Optional API key ID filter + * @returns Usage logs within date range + */ +export async function getByDateRange( + startDate: string, + endDate: string, + apiKeyId?: number +): Promise> { + const params: UsageQueryParams = { + start_date: startDate, + end_date: endDate, + page: 1, + page_size: 100, + }; + + if (apiKeyId !== undefined) { + params.api_key_id = apiKeyId; + } + + const { data } = await apiClient.get>('/usage', { + params, + }); + return data; +} + +/** + * Get detailed usage log by ID + * @param id - Usage log ID + * @returns Usage log details + */ +export async function getById(id: number): Promise { + const { data } = await apiClient.get(`/usage/${id}`); + return data; +} + +// ==================== Dashboard API ==================== + +/** + * Get user dashboard statistics + * @returns Dashboard statistics for current user + */ +export async function getDashboardStats(): Promise { + const { data } = await apiClient.get('/usage/dashboard/stats'); + return data; +} + +/** + * Get user usage trend data + * @param params - Query parameters for filtering + * @returns Usage trend data for current user + */ +export async function getDashboardTrend(params?: TrendParams): Promise { + const { data } = await apiClient.get('/usage/dashboard/trend', { params }); + return data; +} + +/** + * Get user model usage statistics + * @param params - Query parameters for filtering + * @returns Model usage statistics for current user + */ +export async function getDashboardModels(params?: { start_date?: string; end_date?: string }): Promise { + const { data } = await apiClient.get('/usage/dashboard/models', { params }); + return data; +} + +export interface BatchApiKeyUsageStats { + api_key_id: number; + today_actual_cost: number; + total_actual_cost: number; +} + +export interface BatchApiKeysUsageResponse { + stats: Record; +} + +/** + * Get batch usage stats for user's own API keys + * @param apiKeyIds - Array of API key IDs + * @returns Usage stats map keyed by API key ID + */ +export async function getDashboardApiKeysUsage(apiKeyIds: number[]): Promise { + const { data } = await apiClient.post('/usage/dashboard/api-keys-usage', { + api_key_ids: apiKeyIds, + }); + return data; +} + +export const usageAPI = { + list, + query, + getStats, + getStatsByDateRange, + getByDateRange, + getById, + // Dashboard + getDashboardStats, + getDashboardTrend, + getDashboardModels, + getDashboardApiKeysUsage, +}; + +export default usageAPI; diff --git a/frontend/src/api/user.ts b/frontend/src/api/user.ts new file mode 100644 index 00000000..890c0fb8 --- /dev/null +++ b/frontend/src/api/user.ts @@ -0,0 +1,41 @@ +/** + * User API endpoints + * Handles user profile management and password changes + */ + +import { apiClient } from './client'; +import type { User, ChangePasswordRequest } from '@/types'; + +/** + * Get current user profile + * @returns User profile data + */ +export async function getProfile(): Promise { + const { data } = await apiClient.get('/users/me'); + return data; +} + +/** + * Change current user password + * @param passwords - Old and new password + * @returns Success message + */ +export async function changePassword( + oldPassword: string, + newPassword: string +): Promise<{ message: string }> { + const payload: ChangePasswordRequest = { + old_password: oldPassword, + new_password: newPassword, + }; + + const { data } = await apiClient.post<{ message: string }>('/users/me/password', payload); + return data; +} + +export const userAPI = { + getProfile, + changePassword, +}; + +export default userAPI; diff --git a/frontend/src/components/TurnstileWidget.vue b/frontend/src/components/TurnstileWidget.vue new file mode 100644 index 00000000..e64a132a --- /dev/null +++ b/frontend/src/components/TurnstileWidget.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/frontend/src/components/account/AccountStatusIndicator.vue b/frontend/src/components/account/AccountStatusIndicator.vue new file mode 100644 index 00000000..7b000ee9 --- /dev/null +++ b/frontend/src/components/account/AccountStatusIndicator.vue @@ -0,0 +1,120 @@ + + + diff --git a/frontend/src/components/account/AccountTestModal.vue b/frontend/src/components/account/AccountTestModal.vue new file mode 100644 index 00000000..ec48f956 --- /dev/null +++ b/frontend/src/components/account/AccountTestModal.vue @@ -0,0 +1,342 @@ + + + diff --git a/frontend/src/components/account/AccountTodayStatsCell.vue b/frontend/src/components/account/AccountTodayStatsCell.vue new file mode 100644 index 00000000..84a2dcdf --- /dev/null +++ b/frontend/src/components/account/AccountTodayStatsCell.vue @@ -0,0 +1,82 @@ + + + diff --git a/frontend/src/components/account/AccountUsageCell.vue b/frontend/src/components/account/AccountUsageCell.vue new file mode 100644 index 00000000..71c1a05f --- /dev/null +++ b/frontend/src/components/account/AccountUsageCell.vue @@ -0,0 +1,113 @@ + + + diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue new file mode 100644 index 00000000..45336d6b --- /dev/null +++ b/frontend/src/components/account/CreateAccountModal.vue @@ -0,0 +1,929 @@ + + + diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue new file mode 100644 index 00000000..18789be3 --- /dev/null +++ b/frontend/src/components/account/EditAccountModal.vue @@ -0,0 +1,646 @@ + + + diff --git a/frontend/src/components/account/OAuthAuthorizationFlow.vue b/frontend/src/components/account/OAuthAuthorizationFlow.vue new file mode 100644 index 00000000..5d9320a8 --- /dev/null +++ b/frontend/src/components/account/OAuthAuthorizationFlow.vue @@ -0,0 +1,364 @@ + + + diff --git a/frontend/src/components/account/ReAuthAccountModal.vue b/frontend/src/components/account/ReAuthAccountModal.vue new file mode 100644 index 00000000..e3fd5b94 --- /dev/null +++ b/frontend/src/components/account/ReAuthAccountModal.vue @@ -0,0 +1,240 @@ + + + diff --git a/frontend/src/components/account/SetupTokenTimeWindow.vue b/frontend/src/components/account/SetupTokenTimeWindow.vue new file mode 100644 index 00000000..8364a3b5 --- /dev/null +++ b/frontend/src/components/account/SetupTokenTimeWindow.vue @@ -0,0 +1,200 @@ + + + diff --git a/frontend/src/components/account/UsageProgressBar.vue b/frontend/src/components/account/UsageProgressBar.vue new file mode 100644 index 00000000..6b185176 --- /dev/null +++ b/frontend/src/components/account/UsageProgressBar.vue @@ -0,0 +1,129 @@ + + + diff --git a/frontend/src/components/account/index.ts b/frontend/src/components/account/index.ts new file mode 100644 index 00000000..7dc376c1 --- /dev/null +++ b/frontend/src/components/account/index.ts @@ -0,0 +1,7 @@ +export { default as CreateAccountModal } from './CreateAccountModal.vue' +export { default as EditAccountModal } from './EditAccountModal.vue' +export { default as ReAuthAccountModal } from './ReAuthAccountModal.vue' +export { default as OAuthAuthorizationFlow } from './OAuthAuthorizationFlow.vue' +export { default as AccountStatusIndicator } from './AccountStatusIndicator.vue' +export { default as AccountUsageCell } from './AccountUsageCell.vue' +export { default as UsageProgressBar } from './UsageProgressBar.vue' diff --git a/frontend/src/components/common/ConfirmDialog.vue b/frontend/src/components/common/ConfirmDialog.vue new file mode 100644 index 00000000..a345356a --- /dev/null +++ b/frontend/src/components/common/ConfirmDialog.vue @@ -0,0 +1,65 @@ + + + diff --git a/frontend/src/components/common/DataTable.vue b/frontend/src/components/common/DataTable.vue new file mode 100644 index 00000000..df728805 --- /dev/null +++ b/frontend/src/components/common/DataTable.vue @@ -0,0 +1,134 @@ + + + diff --git a/frontend/src/components/common/DateRangePicker.vue b/frontend/src/components/common/DateRangePicker.vue new file mode 100644 index 00000000..3f99bd1d --- /dev/null +++ b/frontend/src/components/common/DateRangePicker.vue @@ -0,0 +1,415 @@ + + + + + diff --git a/frontend/src/components/common/EmptyState.vue b/frontend/src/components/common/EmptyState.vue new file mode 100644 index 00000000..4081fec2 --- /dev/null +++ b/frontend/src/components/common/EmptyState.vue @@ -0,0 +1,91 @@ + + + diff --git a/frontend/src/components/common/GroupBadge.vue b/frontend/src/components/common/GroupBadge.vue new file mode 100644 index 00000000..3f957c6e --- /dev/null +++ b/frontend/src/components/common/GroupBadge.vue @@ -0,0 +1,50 @@ + + + diff --git a/frontend/src/components/common/GroupSelector.vue b/frontend/src/components/common/GroupSelector.vue new file mode 100644 index 00000000..44d783be --- /dev/null +++ b/frontend/src/components/common/GroupSelector.vue @@ -0,0 +1,61 @@ + + + diff --git a/frontend/src/components/common/LoadingSpinner.vue b/frontend/src/components/common/LoadingSpinner.vue new file mode 100644 index 00000000..b368ba58 --- /dev/null +++ b/frontend/src/components/common/LoadingSpinner.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/frontend/src/components/common/LocaleSwitcher.vue b/frontend/src/components/common/LocaleSwitcher.vue new file mode 100644 index 00000000..b0ce20a5 --- /dev/null +++ b/frontend/src/components/common/LocaleSwitcher.vue @@ -0,0 +1,100 @@ + + + + + diff --git a/frontend/src/components/common/Modal.vue b/frontend/src/components/common/Modal.vue new file mode 100644 index 00000000..9205233d --- /dev/null +++ b/frontend/src/components/common/Modal.vue @@ -0,0 +1,122 @@ + + + diff --git a/frontend/src/components/common/Pagination.vue b/frontend/src/components/common/Pagination.vue new file mode 100644 index 00000000..4011f007 --- /dev/null +++ b/frontend/src/components/common/Pagination.vue @@ -0,0 +1,214 @@ + + + + + diff --git a/frontend/src/components/common/ProxySelector.vue b/frontend/src/components/common/ProxySelector.vue new file mode 100644 index 00000000..d5426622 --- /dev/null +++ b/frontend/src/components/common/ProxySelector.vue @@ -0,0 +1,426 @@ + + + + + diff --git a/frontend/src/components/common/README.md b/frontend/src/components/common/README.md new file mode 100644 index 00000000..02ca0b4d --- /dev/null +++ b/frontend/src/components/common/README.md @@ -0,0 +1,243 @@ +# Common Components + +This directory contains reusable Vue 3 components built with Composition API, TypeScript, and TailwindCSS. + +## Components + +### DataTable.vue +A generic data table component with sorting, loading states, and custom cell rendering. + +**Props:** +- `columns: Column[]` - Array of column definitions with key, label, sortable, and formatter +- `data: any[]` - Array of data objects to display +- `loading?: boolean` - Show loading skeleton + +**Slots:** +- `empty` - Custom empty state content +- `cell-{key}` - Custom cell renderer for specific column (receives `row` and `value`) + +**Usage:** +```vue + + + +``` + +--- + +### Pagination.vue +Pagination component with page numbers, navigation, and page size selector. + +**Props:** +- `total: number` - Total number of items +- `page: number` - Current page (1-indexed) +- `pageSize: number` - Items per page +- `pageSizeOptions?: number[]` - Available page size options (default: [10, 20, 50, 100]) + +**Events:** +- `update:page` - Emitted when page changes +- `update:pageSize` - Emitted when page size changes + +**Usage:** +```vue + +``` + +--- + +### Modal.vue +Modal dialog with customizable size and close behavior. + +**Props:** +- `show: boolean` - Control modal visibility +- `title: string` - Modal title +- `size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'` - Modal size (default: 'md') +- `closeOnEscape?: boolean` - Close on Escape key (default: true) +- `closeOnClickOutside?: boolean` - Close on backdrop click (default: true) + +**Events:** +- `close` - Emitted when modal should close + +**Slots:** +- `default` - Modal body content +- `footer` - Modal footer content + +**Usage:** +```vue + +

+ + + + + +``` + +--- + +### ConfirmDialog.vue +Confirmation dialog built on top of Modal component. + +**Props:** +- `show: boolean` - Control dialog visibility +- `title: string` - Dialog title +- `message: string` - Confirmation message +- `confirmText?: string` - Confirm button text (default: 'Confirm') +- `cancelText?: string` - Cancel button text (default: 'Cancel') +- `danger?: boolean` - Use danger/red styling (default: false) + +**Events:** +- `confirm` - Emitted when user confirms +- `cancel` - Emitted when user cancels + +**Usage:** +```vue + +``` + +--- + +### StatCard.vue +Statistics card component for displaying metrics with optional change indicators. + +**Props:** +- `title: string` - Card title +- `value: number | string` - Main value to display +- `icon?: Component` - Icon component +- `change?: number` - Percentage change value +- `changeType?: 'up' | 'down' | 'neutral'` - Change direction (default: 'neutral') +- `formatValue?: (value) => string` - Custom value formatter + +**Usage:** +```vue + +``` + +--- + +### Toast.vue +Toast notification component that automatically displays toasts from the app store. + +**Usage:** +```vue + + +``` + +```typescript +// Trigger toasts from anywhere using the app store +import { useAppStore } from '@/stores/app' + +const appStore = useAppStore() + +appStore.addToast({ + type: 'success', + title: 'Success!', + message: 'User created successfully', + duration: 3000 +}) + +appStore.addToast({ + type: 'error', + message: 'Failed to delete user' +}) +``` + +--- + +### LoadingSpinner.vue +Simple animated loading spinner. + +**Props:** +- `size?: 'sm' | 'md' | 'lg' | 'xl'` - Spinner size (default: 'md') +- `color?: 'primary' | 'secondary' | 'white' | 'gray'` - Spinner color (default: 'primary') + +**Usage:** +```vue + +``` + +--- + +### EmptyState.vue +Empty state placeholder with icon, message, and optional action button. + +**Props:** +- `icon?: Component` - Icon component +- `title: string` - Empty state title +- `description: string` - Empty state description +- `actionText?: string` - Action button text +- `actionTo?: string | object` - Router link destination +- `actionIcon?: boolean` - Show plus icon in button (default: true) + +**Slots:** +- `icon` - Custom icon content +- `action` - Custom action button/link + +**Usage:** +```vue + +``` + +## Import + +You can import components individually: + +```typescript +import { DataTable, Pagination, Modal } from '@/components/common' +``` + +Or import specific components: + +```typescript +import DataTable from '@/components/common/DataTable.vue' +``` + +## Features + +All components include: +- **TypeScript support** with proper type definitions +- **Accessibility** with ARIA attributes and keyboard navigation +- **Responsive design** with mobile-friendly layouts +- **TailwindCSS styling** for consistent design +- **Vue 3 Composition API** with ` + + diff --git a/frontend/src/components/common/StatCard.vue b/frontend/src/components/common/StatCard.vue new file mode 100644 index 00000000..a82bea86 --- /dev/null +++ b/frontend/src/components/common/StatCard.vue @@ -0,0 +1,94 @@ + + + diff --git a/frontend/src/components/common/SubscriptionProgressMini.vue b/frontend/src/components/common/SubscriptionProgressMini.vue new file mode 100644 index 00000000..46d32d6d --- /dev/null +++ b/frontend/src/components/common/SubscriptionProgressMini.vue @@ -0,0 +1,267 @@ + + + + + diff --git a/frontend/src/components/common/Toast.vue b/frontend/src/components/common/Toast.vue new file mode 100644 index 00000000..ba5dea27 --- /dev/null +++ b/frontend/src/components/common/Toast.vue @@ -0,0 +1,224 @@ + + + diff --git a/frontend/src/components/common/Toggle.vue b/frontend/src/components/common/Toggle.vue new file mode 100644 index 00000000..460afa00 --- /dev/null +++ b/frontend/src/components/common/Toggle.vue @@ -0,0 +1,35 @@ + + + diff --git a/frontend/src/components/common/VersionBadge.vue b/frontend/src/components/common/VersionBadge.vue new file mode 100644 index 00000000..8b86728a --- /dev/null +++ b/frontend/src/components/common/VersionBadge.vue @@ -0,0 +1,250 @@ + + + + + diff --git a/frontend/src/components/common/index.ts b/frontend/src/components/common/index.ts new file mode 100644 index 00000000..725b4273 --- /dev/null +++ b/frontend/src/components/common/index.ts @@ -0,0 +1,13 @@ +// Export all common components +export { default as DataTable } from './DataTable.vue' +export { default as Pagination } from './Pagination.vue' +export { default as Modal } from './Modal.vue' +export { default as ConfirmDialog } from './ConfirmDialog.vue' +export { default as StatCard } from './StatCard.vue' +export { default as Toast } from './Toast.vue' +export { default as LoadingSpinner } from './LoadingSpinner.vue' +export { default as EmptyState } from './EmptyState.vue' +export { default as LocaleSwitcher } from './LocaleSwitcher.vue' + +// Export types +export type { Column } from './DataTable.vue' diff --git a/frontend/src/components/keys/UseKeyModal.vue b/frontend/src/components/keys/UseKeyModal.vue new file mode 100644 index 00000000..4c45550c --- /dev/null +++ b/frontend/src/components/keys/UseKeyModal.vue @@ -0,0 +1,200 @@ + + + diff --git a/frontend/src/components/layout/AppHeader.vue b/frontend/src/components/layout/AppHeader.vue new file mode 100644 index 00000000..a4282b5c --- /dev/null +++ b/frontend/src/components/layout/AppHeader.vue @@ -0,0 +1,259 @@ + + + + + diff --git a/frontend/src/components/layout/AppLayout.vue b/frontend/src/components/layout/AppLayout.vue new file mode 100644 index 00000000..bbf5653b --- /dev/null +++ b/frontend/src/components/layout/AppLayout.vue @@ -0,0 +1,36 @@ + + + + diff --git a/frontend/src/components/layout/AppSidebar.vue b/frontend/src/components/layout/AppSidebar.vue new file mode 100644 index 00000000..3ae63abe --- /dev/null +++ b/frontend/src/components/layout/AppSidebar.vue @@ -0,0 +1,331 @@ + + + + + diff --git a/frontend/src/components/layout/AuthLayout.vue b/frontend/src/components/layout/AuthLayout.vue new file mode 100644 index 00000000..9cb6dc48 --- /dev/null +++ b/frontend/src/components/layout/AuthLayout.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/frontend/src/components/layout/EXAMPLES.md b/frontend/src/components/layout/EXAMPLES.md new file mode 100644 index 00000000..ce5469d5 --- /dev/null +++ b/frontend/src/components/layout/EXAMPLES.md @@ -0,0 +1,424 @@ +# Layout Component Examples + +## Example 1: Dashboard Page + +```vue + + + +``` + +--- + +## Example 2: Login Page + +```vue + + + +``` + +--- + +## Example 3: API Keys Page with Custom Header Title + +```vue + + + +``` + +--- + +## Example 4: Admin Users Page + +```vue + + + +``` + +--- + +## Example 5: Profile Page + +```vue + + + +``` + +--- + +## Tips for Using Layouts + +1. **Page Titles**: Set route meta to automatically display page titles in the header +2. **Loading States**: Use `appStore.setLoading(true/false)` for global loading indicators +3. **Toast Notifications**: Use `appStore.showSuccess()`, `appStore.showError()`, etc. +4. **Authentication**: All authenticated pages should use `AppLayout` +5. **Auth Pages**: Login and Register pages should use `AuthLayout` +6. **Sidebar State**: The sidebar state persists across navigation +7. **Mobile First**: All examples are responsive by default using Tailwind's mobile-first approach diff --git a/frontend/src/components/layout/INTEGRATION.md b/frontend/src/components/layout/INTEGRATION.md new file mode 100644 index 00000000..41d1fc57 --- /dev/null +++ b/frontend/src/components/layout/INTEGRATION.md @@ -0,0 +1,480 @@ +# Layout Components Integration Guide + +## Quick Start + +### 1. Import Layout Components + +```typescript +// In your view files +import { AppLayout, AuthLayout } from '@/components/layout'; +``` + +### 2. Use in Routes + +```typescript +// src/router/index.ts +import { createRouter, createWebHistory } from 'vue-router'; +import type { RouteRecordRaw } from 'vue-router'; + +// Views +import DashboardView from '@/views/DashboardView.vue'; +import LoginView from '@/views/auth/LoginView.vue'; +import RegisterView from '@/views/auth/RegisterView.vue'; + +const routes: RouteRecordRaw[] = [ + // Auth routes (no layout needed - views use AuthLayout internally) + { + path: '/login', + name: 'Login', + component: LoginView, + meta: { requiresAuth: false }, + }, + { + path: '/register', + name: 'Register', + component: RegisterView, + meta: { requiresAuth: false }, + }, + + // User routes (use AppLayout) + { + path: '/dashboard', + name: 'Dashboard', + component: DashboardView, + meta: { requiresAuth: true, title: 'Dashboard' }, + }, + { + path: '/api-keys', + name: 'ApiKeys', + component: () => import('@/views/ApiKeysView.vue'), + meta: { requiresAuth: true, title: 'API Keys' }, + }, + { + path: '/usage', + name: 'Usage', + component: () => import('@/views/UsageView.vue'), + meta: { requiresAuth: true, title: 'Usage Statistics' }, + }, + { + path: '/redeem', + name: 'Redeem', + component: () => import('@/views/RedeemView.vue'), + meta: { requiresAuth: true, title: 'Redeem Code' }, + }, + { + path: '/profile', + name: 'Profile', + component: () => import('@/views/ProfileView.vue'), + meta: { requiresAuth: true, title: 'Profile Settings' }, + }, + + // Admin routes (use AppLayout, admin only) + { + path: '/admin/dashboard', + name: 'AdminDashboard', + component: () => import('@/views/admin/DashboardView.vue'), + meta: { requiresAuth: true, requiresAdmin: true, title: 'Admin Dashboard' }, + }, + { + path: '/admin/users', + name: 'AdminUsers', + component: () => import('@/views/admin/UsersView.vue'), + meta: { requiresAuth: true, requiresAdmin: true, title: 'User Management' }, + }, + { + path: '/admin/groups', + name: 'AdminGroups', + component: () => import('@/views/admin/GroupsView.vue'), + meta: { requiresAuth: true, requiresAdmin: true, title: 'Groups' }, + }, + { + path: '/admin/accounts', + name: 'AdminAccounts', + component: () => import('@/views/admin/AccountsView.vue'), + meta: { requiresAuth: true, requiresAdmin: true, title: 'Accounts' }, + }, + { + path: '/admin/proxies', + name: 'AdminProxies', + component: () => import('@/views/admin/ProxiesView.vue'), + meta: { requiresAuth: true, requiresAdmin: true, title: 'Proxies' }, + }, + { + path: '/admin/redeem-codes', + name: 'AdminRedeemCodes', + component: () => import('@/views/admin/RedeemCodesView.vue'), + meta: { requiresAuth: true, requiresAdmin: true, title: 'Redeem Codes' }, + }, + + // Default redirect + { + path: '/', + redirect: '/dashboard', + }, +]; + +const router = createRouter({ + history: createWebHistory(), + routes, +}); + +// Navigation guards +router.beforeEach((to, from, next) => { + const authStore = useAuthStore(); + + if (to.meta.requiresAuth && !authStore.isAuthenticated) { + // Redirect to login if not authenticated + next('/login'); + } else if (to.meta.requiresAdmin && !authStore.isAdmin) { + // Redirect to dashboard if not admin + next('/dashboard'); + } else { + next(); + } +}); + +export default router; +``` + +### 3. Initialize Stores in main.ts + +```typescript +// src/main.ts +import { createApp } from 'vue'; +import { createPinia } from 'pinia'; +import App from './App.vue'; +import router from './router'; +import './style.css'; + +const app = createApp(App); +const pinia = createPinia(); + +app.use(pinia); +app.use(router); + +// Initialize auth state on app startup +import { useAuthStore } from '@/stores'; +const authStore = useAuthStore(); +authStore.checkAuth(); + +app.mount('#app'); +``` + +### 4. Update App.vue + +```vue + + + + +``` + +--- + +## View Component Templates + +### Authenticated Page Template + +```vue + + + + +``` + +### Auth Page Template + +```vue + + + + +``` + +--- + +## Customization + +### Changing Colors + +The components use Tailwind's indigo color scheme by default. To change: + +```vue + +
+
+``` + +### Adding Custom Icons + +Replace HTML entity icons with your preferred icon library: + +```vue + +📈 + + + +``` + +### Sidebar Customization + +Modify navigation items in `AppSidebar.vue`: + +```typescript +// Add/remove/modify navigation items +const userNavItems = [ + { path: '/dashboard', label: 'Dashboard', icon: '📈' }, + { path: '/new-page', label: 'New Page', icon: '📄' }, // Add new item + // ... +]; +``` + +### Header Customization + +Modify user dropdown in `AppHeader.vue`: + +```vue + + + + Settings + +``` + +--- + +## Mobile Responsive Behavior + +### Sidebar +- **Desktop (md+)**: Always visible, can be collapsed to icon-only view +- **Mobile**: Hidden by default, shown via menu toggle in header + +### Header +- **Desktop**: Shows full user info and balance +- **Mobile**: Shows compact view with hamburger menu + +To improve mobile experience, you can add overlay and transitions: + +```vue + + + + +
+``` + +--- + +## State Management Integration + +### Auth Store Usage + +```typescript +import { useAuthStore } from '@/stores'; + +const authStore = useAuthStore(); + +// Check if user is authenticated +if (authStore.isAuthenticated) { + // User is logged in +} + +// Check if user is admin +if (authStore.isAdmin) { + // User has admin role +} + +// Get current user +const user = authStore.user; +``` + +### App Store Usage + +```typescript +import { useAppStore } from '@/stores'; + +const appStore = useAppStore(); + +// Toggle sidebar +appStore.toggleSidebar(); + +// Show notifications +appStore.showSuccess('Operation completed!'); +appStore.showError('Something went wrong'); +appStore.showInfo('Did you know...'); +appStore.showWarning('Be careful!'); + +// Loading state +appStore.setLoading(true); +// ... perform operation +appStore.setLoading(false); + +// Or use helper +await appStore.withLoading(async () => { + // Your async operation +}); +``` + +--- + +## Accessibility Features + +All layout components include: + +- **Semantic HTML**: Proper use of `