@jsonstudio/rcc 0.89.1205 → 0.89.1457
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -1412
- package/configsamples/config.json +426 -0
- package/configsamples/config.reference.json +58 -0
- package/configsamples/provider/crs/config.v1.json +46 -0
- package/configsamples/provider/glm/config.v1.json +81 -0
- package/configsamples/provider/glm-anthropic/config.v1.json +45 -0
- package/configsamples/provider/iflow/config.v1.json +74 -0
- package/configsamples/provider/kimi/config.v1.json +41 -0
- package/configsamples/provider/lmstudio/config.v1.json +101 -0
- package/configsamples/provider/mimo/config.v1.json +35 -0
- package/configsamples/provider/modelscope/config.v1.json +96 -0
- package/configsamples/provider/qwen/config.v1.json +38 -0
- package/configsamples/provider/tab/config.v1.json +50 -0
- package/configsamples/provider/tabglm/config.v1.json +49 -0
- package/dist/build-info.js +2 -2
- package/dist/cli/commands/code.js +12 -6
- package/dist/cli/commands/code.js.map +1 -1
- package/dist/cli/commands/config.d.ts +2 -1
- package/dist/cli/commands/config.js +77 -103
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/examples.js +6 -6
- package/dist/cli/commands/examples.js.map +1 -1
- package/dist/cli/commands/init.d.ts +28 -0
- package/dist/cli/commands/init.js +94 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/port.js +10 -2
- package/dist/cli/commands/port.js.map +1 -1
- package/dist/cli/commands/restart.js +5 -2
- package/dist/cli/commands/restart.js.map +1 -1
- package/dist/cli/commands/start.js +25 -22
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/status.js +1 -0
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/stop.js +1 -0
- package/dist/cli/commands/stop.js.map +1 -1
- package/dist/cli/config/bundled-docs.d.ts +20 -0
- package/dist/cli/config/bundled-docs.js +91 -0
- package/dist/cli/config/bundled-docs.js.map +1 -0
- package/dist/cli/config/init-config.d.ts +37 -0
- package/dist/cli/config/init-config.js +212 -0
- package/dist/cli/config/init-config.js.map +1 -0
- package/dist/cli/config/init-provider-catalog.d.ts +8 -0
- package/dist/cli/config/init-provider-catalog.js +187 -0
- package/dist/cli/config/init-provider-catalog.js.map +1 -0
- package/dist/cli/register/init-command.d.ts +3 -0
- package/dist/cli/register/init-command.js +5 -0
- package/dist/cli/register/init-command.js.map +1 -0
- package/dist/cli.js +28 -3
- package/dist/cli.js.map +1 -1
- package/dist/client/gemini/gemini-protocol-client.js +2 -1
- package/dist/client/gemini/gemini-protocol-client.js.map +1 -1
- package/dist/client/gemini-cli/gemini-cli-protocol-client.js +40 -16
- package/dist/client/gemini-cli/gemini-cli-protocol-client.js.map +1 -1
- package/dist/client/openai/chat-protocol-client.js +2 -1
- package/dist/client/openai/chat-protocol-client.js.map +1 -1
- package/dist/client/responses/responses-protocol-client.js +2 -1
- package/dist/client/responses/responses-protocol-client.js.map +1 -1
- package/dist/config/risk-control-config.d.ts +94 -0
- package/dist/config/risk-control-config.js +196 -0
- package/dist/config/risk-control-config.js.map +1 -0
- package/dist/constants/index.d.ts +6 -0
- package/dist/constants/index.js +13 -0
- package/dist/constants/index.js.map +1 -1
- package/dist/docs/daemon-admin-ui.html +2113 -190
- package/dist/error-handling/quiet-error-handling-center.js +46 -8
- package/dist/error-handling/quiet-error-handling-center.js.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/manager/modules/health/index.d.ts +1 -1
- package/dist/manager/modules/quota/antigravity-quota-manager.d.ts +70 -0
- package/dist/manager/modules/quota/antigravity-quota-manager.js +442 -0
- package/dist/manager/modules/quota/antigravity-quota-manager.js.map +1 -0
- package/dist/manager/modules/quota/index.d.ts +3 -127
- package/dist/manager/modules/quota/index.js +2 -1093
- package/dist/manager/modules/quota/index.js.map +1 -1
- package/dist/manager/modules/quota/provider-key-normalization.d.ts +3 -0
- package/dist/manager/modules/quota/provider-key-normalization.js +155 -0
- package/dist/manager/modules/quota/provider-key-normalization.js.map +1 -0
- package/dist/manager/modules/quota/provider-quota-daemon.cooldown.d.ts +9 -0
- package/dist/manager/modules/quota/provider-quota-daemon.cooldown.js +115 -0
- package/dist/manager/modules/quota/provider-quota-daemon.cooldown.js.map +1 -0
- package/dist/manager/modules/quota/provider-quota-daemon.d.ts +77 -0
- package/dist/manager/modules/quota/provider-quota-daemon.events.d.ts +12 -0
- package/dist/manager/modules/quota/provider-quota-daemon.events.js +239 -0
- package/dist/manager/modules/quota/provider-quota-daemon.events.js.map +1 -0
- package/dist/manager/modules/quota/provider-quota-daemon.js +404 -0
- package/dist/manager/modules/quota/provider-quota-daemon.js.map +1 -0
- package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.d.ts +11 -0
- package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.js +192 -0
- package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.js.map +1 -0
- package/dist/manager/modules/quota/provider-quota-daemon.snapshot.d.ts +8 -0
- package/dist/manager/modules/quota/provider-quota-daemon.snapshot.js +96 -0
- package/dist/manager/modules/quota/provider-quota-daemon.snapshot.js.map +1 -0
- package/dist/manager/modules/quota/provider-quota-daemon.view.d.ts +19 -0
- package/dist/manager/modules/quota/provider-quota-daemon.view.js +37 -0
- package/dist/manager/modules/quota/provider-quota-daemon.view.js.map +1 -0
- package/dist/manager/modules/routing/index.d.ts +1 -0
- package/dist/manager/modules/routing/index.js +11 -25
- package/dist/manager/modules/routing/index.js.map +1 -1
- package/dist/manager/quota/provider-quota-center.d.ts +2 -0
- package/dist/manager/quota/provider-quota-center.js +80 -82
- package/dist/manager/quota/provider-quota-center.js.map +1 -1
- package/dist/modules/llmswitch/bridge.d.ts +16 -18
- package/dist/modules/llmswitch/bridge.js +293 -94
- package/dist/modules/llmswitch/bridge.js.map +1 -1
- package/dist/modules/llmswitch/core-loader.d.ts +4 -2
- package/dist/modules/llmswitch/core-loader.js +32 -20
- package/dist/modules/llmswitch/core-loader.js.map +1 -1
- package/dist/modules/pipeline/utils/colored-logger.js +3 -2
- package/dist/modules/pipeline/utils/colored-logger.js.map +1 -1
- package/dist/modules/pipeline/utils/debug-logger.js +1 -1
- package/dist/modules/pipeline/utils/debug-logger.js.map +1 -1
- package/dist/providers/auth/antigravity-userinfo-helper.d.ts +2 -1
- package/dist/providers/auth/antigravity-userinfo-helper.js +25 -4
- package/dist/providers/auth/antigravity-userinfo-helper.js.map +1 -1
- package/dist/providers/auth/iflow-cookie-auth.js +0 -2
- package/dist/providers/auth/iflow-cookie-auth.js.map +1 -1
- package/dist/providers/auth/oauth-lifecycle.js +2 -23
- package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
- package/dist/providers/auth/tokenfile-auth.d.ts +2 -0
- package/dist/providers/auth/tokenfile-auth.js +33 -1
- package/dist/providers/auth/tokenfile-auth.js.map +1 -1
- package/dist/providers/core/config/camoufox-launcher.d.ts +5 -0
- package/dist/providers/core/config/camoufox-launcher.js +40 -4
- package/dist/providers/core/config/camoufox-launcher.js.map +1 -1
- package/dist/providers/core/config/service-profiles.js +7 -18
- package/dist/providers/core/config/service-profiles.js.map +1 -1
- package/dist/providers/core/runtime/antigravity-quota-client.js +6 -3
- package/dist/providers/core/runtime/antigravity-quota-client.js.map +1 -1
- package/dist/providers/core/runtime/base-provider.d.ts +2 -7
- package/dist/providers/core/runtime/base-provider.js +84 -165
- package/dist/providers/core/runtime/base-provider.js.map +1 -1
- package/dist/providers/core/runtime/gemini-cli-http-provider.d.ts +7 -0
- package/dist/providers/core/runtime/gemini-cli-http-provider.js +368 -97
- package/dist/providers/core/runtime/gemini-cli-http-provider.js.map +1 -1
- package/dist/providers/core/runtime/http-request-executor.d.ts +3 -0
- package/dist/providers/core/runtime/http-request-executor.js +110 -38
- package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
- package/dist/providers/core/runtime/http-transport-provider.d.ts +17 -0
- package/dist/providers/core/runtime/http-transport-provider.js +165 -16
- package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
- package/dist/providers/core/runtime/provider-error-classifier.js +10 -0
- package/dist/providers/core/runtime/provider-error-classifier.js.map +1 -1
- package/dist/providers/core/runtime/provider-factory.js +7 -5
- package/dist/providers/core/runtime/provider-factory.js.map +1 -1
- package/dist/providers/core/runtime/provider-runtime-metadata.d.ts +6 -0
- package/dist/providers/core/runtime/provider-runtime-metadata.js.map +1 -1
- package/dist/providers/core/runtime/rate-limit-manager.d.ts +1 -12
- package/dist/providers/core/runtime/rate-limit-manager.js +4 -77
- package/dist/providers/core/runtime/rate-limit-manager.js.map +1 -1
- package/dist/providers/core/runtime/responses-provider.d.ts +1 -7
- package/dist/providers/core/runtime/responses-provider.js +12 -93
- package/dist/providers/core/runtime/responses-provider.js.map +1 -1
- package/dist/providers/core/strategies/oauth-auth-code-flow.js +12 -8
- package/dist/providers/core/strategies/oauth-auth-code-flow.js.map +1 -1
- package/dist/providers/core/utils/http-client.js +36 -46
- package/dist/providers/core/utils/http-client.js.map +1 -1
- package/dist/providers/core/utils/provider-error-logger.d.ts +1 -1
- package/dist/providers/core/utils/provider-error-reporter.d.ts +3 -1
- package/dist/providers/core/utils/provider-error-reporter.js +3 -0
- package/dist/providers/core/utils/provider-error-reporter.js.map +1 -1
- package/dist/providers/core/utils/snapshot-writer.js +1 -4
- package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
- package/dist/providers/mock/mock-provider-runtime.js +57 -27
- package/dist/providers/mock/mock-provider-runtime.js.map +1 -1
- package/dist/scripts/camoufox/launch-auth.mjs +193 -58
- package/dist/server/handlers/handler-utils.js +8 -3
- package/dist/server/handlers/handler-utils.js.map +1 -1
- package/dist/server/handlers/responses-handler.js +1 -1
- package/dist/server/handlers/responses-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/auth-handler.d.ts +2 -0
- package/dist/server/runtime/http-server/daemon-admin/auth-handler.js +103 -0
- package/dist/server/runtime/http-server/daemon-admin/auth-handler.js.map +1 -0
- package/dist/server/runtime/http-server/daemon-admin/auth-session.d.ts +5 -0
- package/dist/server/runtime/http-server/daemon-admin/auth-session.js +77 -0
- package/dist/server/runtime/http-server/daemon-admin/auth-session.js.map +1 -0
- package/dist/server/runtime/http-server/daemon-admin/auth-store.d.ts +18 -0
- package/dist/server/runtime/http-server/daemon-admin/auth-store.js +89 -0
- package/dist/server/runtime/http-server/daemon-admin/auth-store.js.map +1 -0
- package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +1 -2
- package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +226 -24
- package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/quota-handler.js +47 -8
- package/dist/server/runtime/http-server/daemon-admin/quota-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/restart-handler.js +1 -1
- package/dist/server/runtime/http-server/daemon-admin/restart-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/stats-handler.js +1 -1
- package/dist/server/runtime/http-server/daemon-admin/stats-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/status-handler.js +68 -4
- package/dist/server/runtime/http-server/daemon-admin/status-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin-routes.d.ts +3 -4
- package/dist/server/runtime/http-server/daemon-admin-routes.js +9 -14
- package/dist/server/runtime/http-server/daemon-admin-routes.js.map +1 -1
- package/dist/server/runtime/http-server/executor-metadata.js +1 -1
- package/dist/server/runtime/http-server/executor-metadata.js.map +1 -1
- package/dist/server/runtime/http-server/executor-response.js +0 -16
- package/dist/server/runtime/http-server/executor-response.js.map +1 -1
- package/dist/server/runtime/http-server/hub-shadow-compare.js +110 -34
- package/dist/server/runtime/http-server/hub-shadow-compare.js.map +1 -1
- package/dist/server/runtime/http-server/index.d.ts +5 -3
- package/dist/server/runtime/http-server/index.js +281 -136
- package/dist/server/runtime/http-server/index.js.map +1 -1
- package/dist/server/runtime/http-server/middleware.js +19 -1
- package/dist/server/runtime/http-server/middleware.js.map +1 -1
- package/dist/server/runtime/http-server/request-executor.js +59 -24
- package/dist/server/runtime/http-server/request-executor.js.map +1 -1
- package/dist/server/runtime/http-server/routes.js +12 -3
- package/dist/server/runtime/http-server/routes.js.map +1 -1
- package/dist/server/runtime/http-server/session-dir.d.ts +2 -0
- package/dist/server/runtime/http-server/session-dir.js +59 -0
- package/dist/server/runtime/http-server/session-dir.js.map +1 -0
- package/dist/server/runtime/http-server/types.d.ts +0 -4
- package/dist/server/utils/utf8-chunk-buffer.js +6 -3
- package/dist/server/utils/utf8-chunk-buffer.js.map +1 -1
- package/dist/server/utils/warmup-storm-tracker.js +1 -1
- package/dist/server/utils/warmup-storm-tracker.js.map +1 -1
- package/dist/server-factory.d.ts +6 -28
- package/dist/server-factory.js +8 -93
- package/dist/server-factory.js.map +1 -1
- package/dist/token-daemon/index.js +2 -2
- package/dist/token-daemon/index.js.map +1 -1
- package/dist/token-daemon/provider-registry.js +0 -1
- package/dist/token-daemon/provider-registry.js.map +1 -1
- package/dist/token-daemon/server-utils.js +8 -9
- package/dist/token-daemon/server-utils.js.map +1 -1
- package/dist/token-daemon/token-utils.js +1 -1
- package/dist/token-daemon/token-utils.js.map +1 -1
- package/dist/tools/semantic-replay.js +2 -2
- package/dist/tools/semantic-replay.js.map +1 -1
- package/dist/tools/stats-request-events.d.ts +1 -1
- package/dist/tools/stats-usage.js +6 -3
- package/dist/tools/stats-usage.js.map +1 -1
- package/dist/utils/llms-engine-shadow.d.ts +19 -0
- package/dist/utils/llms-engine-shadow.js +209 -0
- package/dist/utils/llms-engine-shadow.js.map +1 -0
- package/dist/utils/runtime-versions.js +2 -1
- package/dist/utils/runtime-versions.js.map +1 -1
- package/dist/utils/strip-internal-keys.d.ts +12 -0
- package/dist/utils/strip-internal-keys.js +28 -0
- package/dist/utils/strip-internal-keys.js.map +1 -0
- package/docs/ARCHITECTURE.md +402 -0
- package/docs/CHAT_PROCESS_PROTOCOL_AND_PIPELINE.md +221 -0
- package/docs/CODEX_AND_CLAUDE_CODE.md +69 -0
- package/docs/CONFIG_ARCHITECTURE.md +517 -0
- package/docs/ERROR_HANDLING_AUDIT.md +0 -0
- package/docs/GCLI2API_PARITY_GAPS.md +98 -0
- package/docs/INSTALLATION_AND_QUICKSTART.md +74 -0
- package/docs/INSTRUCTION_MARKUP.md +89 -0
- package/docs/MODULE_ENHANCEMENT_SYSTEM.md +666 -0
- package/docs/PORTS.md +36 -0
- package/docs/PROVIDERS_BUILTIN.md +111 -0
- package/docs/PROVIDER_TYPES.md +55 -0
- package/docs/SERVERTOOL_CLOCK_DESIGN.md +233 -0
- package/docs/USAGE_HANDLING_ANALYSIS.md +335 -0
- package/docs/USER_CONFIG_PARSER_CHANGES.md +175 -0
- package/docs/V3_INBOUND_OUTBOUND_DESIGN.md +86 -0
- package/docs/VIRTUAL_ROUTER_PRIORITY_AND_HEALTH.md +125 -0
- package/docs/anthropic-request-golden-samples.md +50 -0
- package/docs/antigravity-gemini-format-cleanup.md +102 -0
- package/docs/antigravity-routing-contract.md +31 -0
- package/docs/ccr-alignment-enhancetool.md +105 -0
- package/docs/chat-glm-500-analysis.md +79 -0
- package/docs/chat-request-golden-samples.md +42 -0
- package/docs/chat-semantic-expansion-plan.md +84 -0
- package/docs/cli-command-inventory.md +76 -0
- package/docs/codex-samples-replay.md +50 -0
- package/docs/daemon-admin-api-design.md +350 -0
- package/docs/daemon-admin-module-structure.md +169 -0
- package/docs/daemon-admin-ui.html +3394 -0
- package/docs/debug-system-design.md +734 -0
- package/docs/debugging/gemini-sse-root-cause.md +52 -0
- package/docs/debugging/sse_encoding_failure_analysis.md +53 -0
- package/docs/dry-run/README.md +721 -0
- package/docs/error-handling-v2.md +92 -0
- package/docs/exec-command-guard-policy.example.v1.json +42 -0
- package/docs/fixes/gemini-protocol-mapping.md +57 -0
- package/docs/fixes/oauth-portal-timing-fix.md +202 -0
- package/docs/fixes/web-search-hop3-fix.md +265 -0
- package/docs/glm-api-reference.md +390 -0
- package/docs/glm-chat-completions.md +1779 -0
- package/docs/glm-history-inline-images.md +44 -0
- package/docs/golden-ci-library.md +66 -0
- package/docs/lmstudio-dry-run-summary.md +203 -0
- package/docs/lmstudio-tool-calling.md +214 -0
- package/docs/mapping-tables/anthropic-to-openai.json +290 -0
- package/docs/mapping-tables/iflow-to-openai.json +215 -0
- package/docs/mapping-tables/openai-passthrough.json +190 -0
- package/docs/mapping-tables/openai-to-iflow.json +227 -0
- package/docs/monitoring/Design.md +61 -0
- package/docs/multi-token-auth-guide.md +66 -0
- package/docs/oauth-authentication-guide.md +168 -0
- package/docs/oauth-iflow-implementation.md +153 -0
- package/docs/pipeline-routing-report.md +209 -0
- package/docs/plans/manager-daemon/PLAN.md +86 -0
- package/docs/plans/provider-config-v2-plan.md +176 -0
- package/docs/plans/provider-runtime-manager-plan.md +209 -0
- package/docs/plans/transparent-429-failover.md +89 -0
- package/docs/plans/unified-hub-framework-v1.md +245 -0
- package/docs/provider-config-v2-ui-design.md +181 -0
- package/docs/provider-quota-design.md +129 -0
- package/docs/providers/gemini-provider.md +62 -0
- package/docs/providers/lmstudio-v2-migration-report.md +102 -0
- package/docs/providers/provider-composite-design.md +142 -0
- package/docs/providers/provider-composite-testing.md +98 -0
- package/docs/providers/provider-type-only-migration.md +111 -0
- package/docs/rccx-wasm-migration.md +74 -0
- package/docs/refactoring/architecture-comparison-diagram.md +140 -0
- package/docs/refactoring/compatibility-v2-architecture-design.md +738 -0
- package/docs/refactoring/workflow-compatibility-refactoring-design.md +361 -0
- package/docs/reports/routing-classification-report.json +24 -0
- package/docs/reports/routing-classification-report.md +18 -0
- package/docs/reports/thinking-keywords-report.json +19 -0
- package/docs/responses/README.md +156 -0
- package/docs/responses-generic-provider.md +86 -0
- package/docs/responses-passthrough-provider-design.md +202 -0
- package/docs/routing-awrr-health-weighted-round-robin.md +179 -0
- package/docs/routing-instructions.md +393 -0
- package/docs/servertool-framework.md +65 -0
- package/docs/stop-message-auto.md +225 -0
- package/docs/streaming-flow.html +30 -0
- package/docs/streaming-flow.md +182 -0
- package/docs/token-daemon-preview.html +490 -0
- package/docs/token-refresh-daemon-plan.md +269 -0
- package/docs/transformation-tables/Gemini-FinishReason/345/256/214/346/225/264/350/275/254/346/215/242/350/241/250.json +233 -0
- package/docs/transformation-tables/README.md +225 -0
- package/docs/transformation-tables/claude-code-router-anthropic-to-gemini.json +283 -0
- package/docs/transformation-tables/claude-code-router-anthropic-to-openai.json +208 -0
- package/docs/transformation-tables/claude-code-router-openai-to-anthropic.json +261 -0
- package/docs/transformation-tables/claude-code-router-openai-to-gemini.json +208 -0
- package/docs/transformation-tables/claude-code-router-openai-to-lmstudio.json +182 -0
- package/docs/transformation-tables/claude-code-router-openai-to-ollama.json +250 -0
- package/docs/transformation-tables/claude-code-router-openai-to-textgenwebui.json +295 -0
- package/docs/transformation-tables/claude-code-router-provider-conversions.json +193 -0
- package/docs/transformation-tables//345/256/214/346/225/264/347/232/204/345/267/245/345/205/267/346/211/247/350/241/214/346/265/201/347/250/213/350/275/254/346/215/242/350/241/250.json +299 -0
- package/docs/transformation-tables//345/257/271/350/257/235/345/216/206/345/217/262/347/273/264/346/212/244/345/210/206/346/236/220.md +134 -0
- package/docs/transformation-tables//345/267/245/345/205/267/350/260/203/347/224/250/346/250/241/345/274/217/345/210/206/346/236/220.md +158 -0
- package/docs/transformation-tables//347/212/266/346/200/201/347/256/241/347/220/206/351/234/200/346/261/202/345/210/206/346/236/220.md +175 -0
- package/docs/transformation-tables//351/235/231/346/200/201/350/241/250vs/345/212/250/346/200/201/345/210/206/346/236/220.md +189 -0
- package/docs/transformation-tables//351/235/231/346/200/201/350/241/250/345/207/206/347/241/256/346/200/247/350/257/204/344/274/260.md +179 -0
- package/docs/transformation-tables//351/235/236/346/265/201/345/274/217/345/234/272/346/231/257/345/210/206/346/236/220.md +189 -0
- package/docs/v2-architecture/IMPLEMENTATION-ROADMAP.md +367 -0
- package/docs/v2-architecture/OPTIMIZED-DESIGN.md +827 -0
- package/docs/v2-architecture/PRERUN-CONNECTION-DESIGN.md +716 -0
- package/docs/v2-architecture/README.md +549 -0
- package/docs/verification/modelscope-verify.md +59 -0
- package/docs/verified-configs/README.md +60 -0
- package/docs/verified-configs/v0.45.0/README.md +244 -0
- package/docs/verified-configs/v0.45.0/lmstudio-5521-gpt-oss-20b-mlx.json +135 -0
- package/docs/verified-configs/v0.45.0/merged-config.5521.json +1205 -0
- package/docs/verified-configs/v0.45.0/merged-config.qwen-5522.json +1559 -0
- package/docs/verified-configs/v0.45.0/qwen-5522-qwen3-coder-plus-final.json +221 -0
- package/docs/verified-configs/v0.45.0/qwen-5522-qwen3-coder-plus-fixed.json +242 -0
- package/docs/verified-configs/v0.45.0/qwen-5522-qwen3-coder-plus.json +242 -0
- package/docs/web-search-service-design.md +322 -0
- package/package.json +26 -15
- package/scripts/build-core.mjs +3 -1
- package/scripts/camoufox/launch-auth.mjs +193 -58
- package/scripts/ci/repo-sanity.mjs +138 -0
- package/scripts/mock-provider/run-regressions.mjs +157 -1
- package/scripts/monitor-diff.mjs +126 -0
- package/scripts/pack-mode.mjs +19 -1
- package/scripts/pack-rcc.mjs +63 -0
- package/scripts/run-bg.sh +0 -14
- package/scripts/tests/ci-jest.mjs +119 -0
- package/scripts/tools-dev/responses-debug-client/README.md +23 -0
- package/scripts/tools-dev/responses-debug-client/payloads/poem.json +13 -0
- package/scripts/tools-dev/responses-debug-client/payloads/sample-no-tools.json +98 -0
- package/scripts/tools-dev/responses-debug-client/payloads/text.json +13 -0
- package/scripts/tools-dev/responses-debug-client/payloads/tool.json +27 -0
- package/scripts/tools-dev/responses-debug-client/run.mjs +65 -0
- package/scripts/tools-dev/responses-debug-client/src/index.ts +281 -0
- package/scripts/tools-dev/run-llmswitch-chat.mjs +53 -0
- package/scripts/tools-dev/server-tools-dev/run-web-fetch.mjs +65 -0
- package/scripts/unified-hub-shadow-compare.mjs +33 -13
- package/scripts/vendor-core.mjs +13 -3
- package/scripts/verify-e2e-toolcall.mjs +115 -26
- package/dist/modules/llmswitch/pipeline-registry.d.ts +0 -57
- package/dist/modules/llmswitch/pipeline-registry.js +0 -229
- package/dist/modules/llmswitch/pipeline-registry.js.map +0 -1
- package/dist/server/RouteCodexServer.d.ts +0 -13
- package/dist/server/RouteCodexServer.js +0 -25
- package/dist/server/RouteCodexServer.js.map +0 -1
- package/dist/v2/conversion/hub/snapshot-recorder.d.ts +0 -12
- package/dist/v2/conversion/hub/snapshot-recorder.js +0 -22
- package/dist/v2/conversion/hub/snapshot-recorder.js.map +0 -1
- package/scripts/test-fc-responses.mjs +0 -66
- package/scripts/test-guidance.mjs +0 -100
- package/scripts/test-iflow-web-search.mjs +0 -141
- package/scripts/test-iflow.mjs +0 -379
- package/scripts/test-tool-exec.mjs +0 -26
|
@@ -1,1094 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import { fetchAntigravityQuotaSnapshot, loadAntigravityAccessToken } from '../../../providers/core/runtime/antigravity-quota-client.js';
|
|
5
|
-
import { scanProviderTokenFiles } from '../../../providers/auth/token-scanner/index.js';
|
|
6
|
-
import { resolveAntigravityApiBase } from '../../../providers/auth/antigravity-userinfo-helper.js';
|
|
7
|
-
import { getProviderErrorCenter } from '../../../modules/llmswitch/bridge.js';
|
|
8
|
-
import { readTokenFile, evaluateTokenState } from '../../../token-daemon/token-utils.js';
|
|
9
|
-
import { applyErrorEvent as applyQuotaErrorEvent, applySuccessEvent as applyQuotaSuccessEvent, applyUsageEvent as applyQuotaUsageEvent, createInitialQuotaState, tickQuotaStateTime } from '../../quota/provider-quota-center.js';
|
|
10
|
-
import { appendProviderErrorEvent, loadProviderQuotaSnapshot, saveProviderQuotaSnapshot } from '../../quota/provider-quota-store.js';
|
|
11
|
-
export class QuotaManagerModule {
|
|
12
|
-
id = 'quota';
|
|
13
|
-
snapshot = {};
|
|
14
|
-
antigravityTokens = new Map();
|
|
15
|
-
refreshTimer = null;
|
|
16
|
-
quotaRoutingEnabled = true;
|
|
17
|
-
providerErrorCenter = null;
|
|
18
|
-
async init(context) {
|
|
19
|
-
this.snapshot = this.loadSnapshotFromDisk();
|
|
20
|
-
this.quotaRoutingEnabled = context.quotaRoutingEnabled !== false;
|
|
21
|
-
}
|
|
22
|
-
async start() {
|
|
23
|
-
// 启动时立即做一次最佳努力刷新,然后根据 token 过期时间和 15 分钟基准动态调度后续刷新。
|
|
24
|
-
try {
|
|
25
|
-
await this.refreshAllAntigravityQuotas();
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
// ignore startup refresh failures
|
|
29
|
-
}
|
|
30
|
-
void this.scheduleNextRefresh().catch(() => {
|
|
31
|
-
// ignore scheduling failures
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
async stop() {
|
|
35
|
-
if (this.refreshTimer) {
|
|
36
|
-
clearTimeout(this.refreshTimer);
|
|
37
|
-
this.refreshTimer = null;
|
|
38
|
-
}
|
|
39
|
-
this.saveSnapshotToDisk();
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* 用于 antigravity:注册需要追踪配额的 alias/token。
|
|
43
|
-
* 多次调用同一 alias 会覆盖最新配置。
|
|
44
|
-
*/
|
|
45
|
-
registerAntigravityToken(alias, tokenFile, apiBase) {
|
|
46
|
-
const cleanAlias = alias.trim();
|
|
47
|
-
const cleanToken = tokenFile.trim();
|
|
48
|
-
const cleanBase = apiBase.trim();
|
|
49
|
-
if (!cleanAlias || !cleanToken || !cleanBase) {
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
this.antigravityTokens.set(cleanAlias, {
|
|
53
|
-
alias: cleanAlias,
|
|
54
|
-
tokenFile: cleanToken,
|
|
55
|
-
apiBase: cleanBase
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* 用于 antigravity:根据 alias+model 更新配额快照。
|
|
60
|
-
*/
|
|
61
|
-
updateAntigravityQuota(alias, quota) {
|
|
62
|
-
const aliasId = alias.trim();
|
|
63
|
-
if (!aliasId) {
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
const now = Date.now();
|
|
67
|
-
const next = { ...this.snapshot };
|
|
68
|
-
for (const [modelId, info] of Object.entries(quota.models)) {
|
|
69
|
-
const key = this.buildAntigravityKey(aliasId, modelId);
|
|
70
|
-
const record = {
|
|
71
|
-
remainingFraction: Number.isFinite(info.remainingFraction) ? info.remainingFraction : null,
|
|
72
|
-
fetchedAt: quota.fetchedAt
|
|
73
|
-
};
|
|
74
|
-
const resetAt = this.computeResetAt(info.resetTimeRaw);
|
|
75
|
-
if (resetAt && resetAt > now) {
|
|
76
|
-
record.resetAt = resetAt;
|
|
77
|
-
}
|
|
78
|
-
next[key] = record;
|
|
79
|
-
const providerKey = `antigravity.${aliasId}.${modelId}`;
|
|
80
|
-
if (record.remainingFraction !== null && record.remainingFraction > 0) {
|
|
81
|
-
void this.emitQuotaRecoveryEvent(providerKey, modelId);
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
const cooldownHint = record.resetAt ? Math.max(0, record.resetAt - now) : undefined;
|
|
85
|
-
void this.emitQuotaDepletedEvent(providerKey, modelId, cooldownHint);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
this.snapshot = next;
|
|
89
|
-
this.saveSnapshotToDisk();
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* 判断给定 providerKey+model 是否有可用配额(仅针对 antigravity 语义)。
|
|
93
|
-
*/
|
|
94
|
-
hasQuotaForAntigravity(providerKey, modelId) {
|
|
95
|
-
const alias = this.extractAntigravityAlias(providerKey);
|
|
96
|
-
if (!alias || !modelId) {
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
const key = this.buildAntigravityKey(alias, modelId);
|
|
100
|
-
const record = this.snapshot[key];
|
|
101
|
-
if (!record) {
|
|
102
|
-
// 没有任何配额记录时视为“无配额”,禁止进入路由池。
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
const now = Date.now();
|
|
106
|
-
// 如果已经超过 resetAt,但尚未刷新到新一轮配额,视为配额状态未知,同样禁止。
|
|
107
|
-
if (record.resetAt && record.resetAt <= now) {
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
if (record.remainingFraction === null) {
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
return record.remainingFraction > 0;
|
|
114
|
-
}
|
|
115
|
-
getRawSnapshot() {
|
|
116
|
-
return { ...this.snapshot };
|
|
117
|
-
}
|
|
118
|
-
buildAntigravityKey(alias, modelId) {
|
|
119
|
-
return `antigravity://${alias}/${modelId}`;
|
|
120
|
-
}
|
|
121
|
-
extractAntigravityAlias(providerKey) {
|
|
122
|
-
if (!providerKey || typeof providerKey !== 'string') {
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
const trimmed = providerKey.trim();
|
|
126
|
-
if (!trimmed.toLowerCase().startsWith('antigravity.')) {
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
const segments = trimmed.split('.');
|
|
130
|
-
if (segments.length < 2) {
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
return segments[1];
|
|
134
|
-
}
|
|
135
|
-
computeResetAt(raw) {
|
|
136
|
-
if (!raw || typeof raw !== 'string' || !raw.trim()) {
|
|
137
|
-
return undefined;
|
|
138
|
-
}
|
|
139
|
-
const value = raw.trim();
|
|
140
|
-
try {
|
|
141
|
-
const normalized = value.endsWith('Z') ? value.replace(/Z$/, '+00:00') : value;
|
|
142
|
-
const parsed = Date.parse(normalized);
|
|
143
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
144
|
-
return undefined;
|
|
145
|
-
}
|
|
146
|
-
return parsed;
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
return undefined;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
async refreshAllAntigravityQuotas() {
|
|
153
|
-
await this.syncAntigravityTokensFromDisk();
|
|
154
|
-
if (this.antigravityTokens.size === 0) {
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
for (const { alias, tokenFile, apiBase } of this.antigravityTokens.values()) {
|
|
158
|
-
try {
|
|
159
|
-
const accessToken = await loadAntigravityAccessToken(tokenFile);
|
|
160
|
-
if (!accessToken) {
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
const snapshot = await fetchAntigravityQuotaSnapshot(apiBase, accessToken);
|
|
164
|
-
if (!snapshot) {
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
this.updateAntigravityQuota(alias, snapshot);
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
170
|
-
// 单个 alias 失败不影响其他 alias 的刷新
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* 根据当前 token 池的过期时间和固定 15 分钟基准,动态安排下一次 quota 刷新:
|
|
176
|
-
* - 如有 token 会在 15 分钟内到期,则在该 token 到期时间附近刷新;
|
|
177
|
-
* - 否则按固定 15 分钟间隔刷新。
|
|
178
|
-
*/
|
|
179
|
-
async scheduleNextRefresh() {
|
|
180
|
-
if (this.refreshTimer) {
|
|
181
|
-
clearTimeout(this.refreshTimer);
|
|
182
|
-
this.refreshTimer = null;
|
|
183
|
-
}
|
|
184
|
-
const baseIntervalMs = 15 * 60 * 1000;
|
|
185
|
-
let delayMs = baseIntervalMs;
|
|
186
|
-
try {
|
|
187
|
-
const nextExpiryDelay = await this.computeNextTokenExpiryDelayMs();
|
|
188
|
-
if (nextExpiryDelay !== null && nextExpiryDelay > 0 && nextExpiryDelay < baseIntervalMs) {
|
|
189
|
-
delayMs = nextExpiryDelay;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
catch {
|
|
193
|
-
// 如果计算失败,退回到固定 15 分钟间隔
|
|
194
|
-
delayMs = baseIntervalMs;
|
|
195
|
-
}
|
|
196
|
-
this.refreshTimer = setTimeout(() => {
|
|
197
|
-
void this.refreshAllAntigravityQuotas()
|
|
198
|
-
.catch(() => {
|
|
199
|
-
// ignore refresh failure
|
|
200
|
-
})
|
|
201
|
-
.finally(() => {
|
|
202
|
-
void this.scheduleNextRefresh().catch(() => {
|
|
203
|
-
// ignore reschedule failure
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
}, delayMs);
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* 扫描 antigravity token 文件,计算距离最近一次 token 过期还剩多少毫秒。
|
|
210
|
-
* 若所有 token 都无过期时间或已过期,则返回 null。
|
|
211
|
-
*/
|
|
212
|
-
async computeNextTokenExpiryDelayMs() {
|
|
213
|
-
let matches = [];
|
|
214
|
-
try {
|
|
215
|
-
const raw = await scanProviderTokenFiles('antigravity');
|
|
216
|
-
matches = raw.map((m) => ({ filePath: m.filePath }));
|
|
217
|
-
}
|
|
218
|
-
catch {
|
|
219
|
-
matches = [];
|
|
220
|
-
}
|
|
221
|
-
if (!matches.length) {
|
|
222
|
-
return null;
|
|
223
|
-
}
|
|
224
|
-
const now = Date.now();
|
|
225
|
-
let minDelay = null;
|
|
226
|
-
for (const match of matches) {
|
|
227
|
-
try {
|
|
228
|
-
const token = await readTokenFile(match.filePath);
|
|
229
|
-
const state = evaluateTokenState(token, now);
|
|
230
|
-
const msLeft = state.msUntilExpiry;
|
|
231
|
-
if (msLeft === null || msLeft <= 0) {
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
if (minDelay === null || msLeft < minDelay) {
|
|
235
|
-
minDelay = msLeft;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
catch {
|
|
239
|
-
// ignore single token file errors
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return minDelay;
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* 自动从本地 auth 目录扫描 antigravity OAuth token,并同步到内存注册表。
|
|
246
|
-
* 这确保「每个 token」都能定期刷新 quota,而不依赖额外的显式注册流程。
|
|
247
|
-
*/
|
|
248
|
-
async syncAntigravityTokensFromDisk() {
|
|
249
|
-
let matches = [];
|
|
250
|
-
try {
|
|
251
|
-
matches = await scanProviderTokenFiles('antigravity');
|
|
252
|
-
}
|
|
253
|
-
catch {
|
|
254
|
-
matches = [];
|
|
255
|
-
}
|
|
256
|
-
if (!matches.length) {
|
|
257
|
-
this.antigravityTokens.clear();
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
const base = resolveAntigravityApiBase();
|
|
261
|
-
const next = new Map();
|
|
262
|
-
for (const match of matches) {
|
|
263
|
-
const label = match.alias && match.alias !== 'default'
|
|
264
|
-
? `${match.sequence}-${match.alias}`
|
|
265
|
-
: String(match.sequence);
|
|
266
|
-
const alias = label.trim();
|
|
267
|
-
if (!alias) {
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
next.set(alias, {
|
|
271
|
-
alias,
|
|
272
|
-
tokenFile: match.filePath,
|
|
273
|
-
apiBase: base
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
// 若已有显式注册的 alias,保留其覆盖权
|
|
277
|
-
for (const [alias, reg] of this.antigravityTokens.entries()) {
|
|
278
|
-
if (!next.has(alias)) {
|
|
279
|
-
next.set(alias, reg);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
this.antigravityTokens = next;
|
|
283
|
-
}
|
|
284
|
-
resolveStatePath() {
|
|
285
|
-
const baseDir = path.join(homedir(), '.routecodex', 'state', 'quota');
|
|
286
|
-
try {
|
|
287
|
-
fs.mkdirSync(baseDir, { recursive: true });
|
|
288
|
-
}
|
|
289
|
-
catch {
|
|
290
|
-
// best effort
|
|
291
|
-
}
|
|
292
|
-
return path.join(baseDir, 'antigravity.json');
|
|
293
|
-
}
|
|
294
|
-
loadSnapshotFromDisk() {
|
|
295
|
-
const filePath = this.resolveStatePath();
|
|
296
|
-
try {
|
|
297
|
-
if (!fs.existsSync(filePath)) {
|
|
298
|
-
return {};
|
|
299
|
-
}
|
|
300
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
301
|
-
const parsed = content.trim() ? JSON.parse(content) : {};
|
|
302
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
303
|
-
return {};
|
|
304
|
-
}
|
|
305
|
-
const raw = parsed;
|
|
306
|
-
const result = {};
|
|
307
|
-
for (const [key, value] of Object.entries(raw)) {
|
|
308
|
-
if (!value || typeof value !== 'object') {
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
311
|
-
let remainingFraction = null;
|
|
312
|
-
if (typeof value.remainingFraction === 'number') {
|
|
313
|
-
remainingFraction = value.remainingFraction ?? null;
|
|
314
|
-
}
|
|
315
|
-
let resetAt;
|
|
316
|
-
if (typeof value.resetAt === 'number') {
|
|
317
|
-
resetAt = value.resetAt;
|
|
318
|
-
}
|
|
319
|
-
const fetchedAt = typeof value.fetchedAt === 'number'
|
|
320
|
-
? value.fetchedAt
|
|
321
|
-
: Date.now();
|
|
322
|
-
result[key] = { remainingFraction, resetAt, fetchedAt };
|
|
323
|
-
}
|
|
324
|
-
return result;
|
|
325
|
-
}
|
|
326
|
-
catch {
|
|
327
|
-
return {};
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
saveSnapshotToDisk() {
|
|
331
|
-
const filePath = this.resolveStatePath();
|
|
332
|
-
try {
|
|
333
|
-
fs.writeFileSync(filePath, `${JSON.stringify(this.snapshot, null, 2)}\n`, 'utf8');
|
|
334
|
-
}
|
|
335
|
-
catch {
|
|
336
|
-
// best effort
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
async getProviderErrorCenterInstance() {
|
|
340
|
-
if (this.providerErrorCenter) {
|
|
341
|
-
return this.providerErrorCenter;
|
|
342
|
-
}
|
|
343
|
-
try {
|
|
344
|
-
const center = await getProviderErrorCenter();
|
|
345
|
-
if (center && typeof center.emit === 'function') {
|
|
346
|
-
this.providerErrorCenter = center;
|
|
347
|
-
}
|
|
348
|
-
else {
|
|
349
|
-
this.providerErrorCenter = null;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
catch {
|
|
353
|
-
this.providerErrorCenter = null;
|
|
354
|
-
}
|
|
355
|
-
return this.providerErrorCenter;
|
|
356
|
-
}
|
|
357
|
-
async emitQuotaRecoveryEvent(providerKey, modelId) {
|
|
358
|
-
if (!providerKey || !modelId) {
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
if (!this.quotaRoutingEnabled) {
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
const center = await this.getProviderErrorCenterInstance();
|
|
365
|
-
if (!center) {
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
const now = Date.now();
|
|
369
|
-
const event = {
|
|
370
|
-
code: 'QUOTA_RECOVERY',
|
|
371
|
-
message: 'Quota manager: provider quota refreshed',
|
|
372
|
-
stage: 'quota',
|
|
373
|
-
status: 200,
|
|
374
|
-
recoverable: true,
|
|
375
|
-
runtime: {
|
|
376
|
-
requestId: `quota_${now}`,
|
|
377
|
-
providerKey,
|
|
378
|
-
providerId: 'antigravity'
|
|
379
|
-
},
|
|
380
|
-
timestamp: now,
|
|
381
|
-
details: {
|
|
382
|
-
virtualRouterQuotaRecovery: {
|
|
383
|
-
providerKey,
|
|
384
|
-
reason: `quota>0 for model ${modelId}`,
|
|
385
|
-
source: 'quota-manager'
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
};
|
|
389
|
-
try {
|
|
390
|
-
center.emit(event);
|
|
391
|
-
}
|
|
392
|
-
catch {
|
|
393
|
-
// 忽略 error center 失败,避免影响配额刷新流程
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
async emitQuotaDepletedEvent(providerKey, modelId, cooldownMs) {
|
|
397
|
-
if (!providerKey || !modelId) {
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
if (!this.quotaRoutingEnabled) {
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
const center = await this.getProviderErrorCenterInstance();
|
|
404
|
-
if (!center) {
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
const now = Date.now();
|
|
408
|
-
const detail = {
|
|
409
|
-
virtualRouterQuotaDepleted: {
|
|
410
|
-
providerKey,
|
|
411
|
-
reason: `quota<=0 for model ${modelId}`,
|
|
412
|
-
...(typeof cooldownMs === 'number' && cooldownMs > 0 ? { cooldownMs } : {})
|
|
413
|
-
}
|
|
414
|
-
};
|
|
415
|
-
const event = {
|
|
416
|
-
code: 'QUOTA_DEPLETED',
|
|
417
|
-
message: 'Quota manager: provider quota exhausted',
|
|
418
|
-
stage: 'quota',
|
|
419
|
-
status: 429,
|
|
420
|
-
recoverable: false,
|
|
421
|
-
runtime: {
|
|
422
|
-
requestId: `quota_${now}`,
|
|
423
|
-
providerKey,
|
|
424
|
-
providerId: 'antigravity'
|
|
425
|
-
},
|
|
426
|
-
timestamp: now,
|
|
427
|
-
details: detail
|
|
428
|
-
};
|
|
429
|
-
try {
|
|
430
|
-
center.emit(event);
|
|
431
|
-
}
|
|
432
|
-
catch {
|
|
433
|
-
// ignore emit errors
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
export class ProviderQuotaDaemonModule {
|
|
438
|
-
id = 'provider-quota';
|
|
439
|
-
quotaStates = new Map();
|
|
440
|
-
staticConfigs = new Map();
|
|
441
|
-
unsubscribe = null;
|
|
442
|
-
maintenanceTimer = null;
|
|
443
|
-
persistTimer = null;
|
|
444
|
-
quotaRoutingEnabled = true;
|
|
445
|
-
async loadSnapshotIntoMemory() {
|
|
446
|
-
// Always clear in-memory state first so operator actions like deleting provider-quota.json
|
|
447
|
-
// take effect immediately after a reload.
|
|
448
|
-
this.quotaStates = new Map();
|
|
449
|
-
const snapshot = await loadProviderQuotaSnapshot();
|
|
450
|
-
if (snapshot && snapshot.providers && typeof snapshot.providers === 'object') {
|
|
451
|
-
for (const [providerKey, state] of Object.entries(snapshot.providers)) {
|
|
452
|
-
if (state && typeof state === 'object') {
|
|
453
|
-
this.quotaStates.set(providerKey, normalizeLoadedQuotaState(providerKey, state));
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
if (this.quotaStates.size) {
|
|
458
|
-
const nowMs = Date.now();
|
|
459
|
-
let changed = false;
|
|
460
|
-
for (const [providerKey, state] of this.quotaStates.entries()) {
|
|
461
|
-
const next = tickQuotaStateTime(state, nowMs);
|
|
462
|
-
if (next !== state) {
|
|
463
|
-
this.quotaStates.set(providerKey, next);
|
|
464
|
-
changed = true;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
if (changed) {
|
|
468
|
-
this.schedulePersist(nowMs);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
// Ensure we always have default entries for known providers (seeded via static configs),
|
|
472
|
-
// so deleting the snapshot file doesn't permanently "hide" providers from admin views.
|
|
473
|
-
if (this.staticConfigs.size) {
|
|
474
|
-
const nowMs = Date.now();
|
|
475
|
-
let seeded = false;
|
|
476
|
-
for (const [providerKey, cfg] of this.staticConfigs.entries()) {
|
|
477
|
-
if (this.quotaStates.has(providerKey)) {
|
|
478
|
-
continue;
|
|
479
|
-
}
|
|
480
|
-
this.quotaStates.set(providerKey, createInitialQuotaState(providerKey, cfg, nowMs));
|
|
481
|
-
seeded = true;
|
|
482
|
-
}
|
|
483
|
-
if (seeded) {
|
|
484
|
-
this.schedulePersist(nowMs);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
async init(context) {
|
|
489
|
-
this.quotaRoutingEnabled = context.quotaRoutingEnabled !== false;
|
|
490
|
-
try {
|
|
491
|
-
await this.loadSnapshotIntoMemory();
|
|
492
|
-
}
|
|
493
|
-
catch {
|
|
494
|
-
this.quotaStates = new Map();
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
async reloadFromDisk() {
|
|
498
|
-
await this.loadSnapshotIntoMemory();
|
|
499
|
-
return { loadedAt: Date.now(), providerCount: this.quotaStates.size };
|
|
500
|
-
}
|
|
501
|
-
async reset(options = {}) {
|
|
502
|
-
const nowMs = Date.now();
|
|
503
|
-
this.quotaStates = new Map();
|
|
504
|
-
// Rebuild default quota entries for known providers so routing can recover immediately.
|
|
505
|
-
if (this.staticConfigs.size) {
|
|
506
|
-
for (const [providerKey, cfg] of this.staticConfigs.entries()) {
|
|
507
|
-
this.quotaStates.set(providerKey, createInitialQuotaState(providerKey, cfg, nowMs));
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
const persisted = options.persist !== false;
|
|
511
|
-
if (persisted) {
|
|
512
|
-
try {
|
|
513
|
-
await saveProviderQuotaSnapshot(this.toSnapshotObject(), new Date(nowMs));
|
|
514
|
-
}
|
|
515
|
-
catch {
|
|
516
|
-
// ignore persistence failure
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
return { resetAt: nowMs, persisted };
|
|
520
|
-
}
|
|
521
|
-
async resetProvider(providerKey) {
|
|
522
|
-
const key = typeof providerKey === 'string' ? providerKey.trim() : '';
|
|
523
|
-
if (!key) {
|
|
524
|
-
return null;
|
|
525
|
-
}
|
|
526
|
-
const nowMs = Date.now();
|
|
527
|
-
const next = createInitialQuotaState(key, this.staticConfigs.get(key), nowMs);
|
|
528
|
-
this.quotaStates.set(key, next);
|
|
529
|
-
try {
|
|
530
|
-
await saveProviderQuotaSnapshot(this.toSnapshotObject(), new Date(nowMs));
|
|
531
|
-
}
|
|
532
|
-
catch {
|
|
533
|
-
// ignore persistence failure
|
|
534
|
-
}
|
|
535
|
-
return { providerKey: key, state: next };
|
|
536
|
-
}
|
|
537
|
-
async recoverProvider(providerKey) {
|
|
538
|
-
const key = typeof providerKey === 'string' ? providerKey.trim() : '';
|
|
539
|
-
if (!key) {
|
|
540
|
-
return null;
|
|
541
|
-
}
|
|
542
|
-
const nowMs = Date.now();
|
|
543
|
-
const previous = this.quotaStates.get(key) ??
|
|
544
|
-
createInitialQuotaState(key, this.staticConfigs.get(key), nowMs);
|
|
545
|
-
const next = {
|
|
546
|
-
...previous,
|
|
547
|
-
inPool: true,
|
|
548
|
-
reason: 'ok',
|
|
549
|
-
cooldownUntil: null,
|
|
550
|
-
blacklistUntil: null,
|
|
551
|
-
lastErrorSeries: null,
|
|
552
|
-
consecutiveErrorCount: 0
|
|
553
|
-
};
|
|
554
|
-
this.quotaStates.set(key, next);
|
|
555
|
-
try {
|
|
556
|
-
await saveProviderQuotaSnapshot(this.toSnapshotObject(), new Date(nowMs));
|
|
557
|
-
}
|
|
558
|
-
catch {
|
|
559
|
-
// ignore persistence failure
|
|
560
|
-
}
|
|
561
|
-
return { providerKey: key, state: next };
|
|
562
|
-
}
|
|
563
|
-
async disableProvider(options) {
|
|
564
|
-
const key = typeof options?.providerKey === 'string' ? options.providerKey.trim() : '';
|
|
565
|
-
if (!key) {
|
|
566
|
-
return null;
|
|
567
|
-
}
|
|
568
|
-
const durationMs = typeof options.durationMs === 'number' && Number.isFinite(options.durationMs) && options.durationMs > 0
|
|
569
|
-
? Math.floor(options.durationMs)
|
|
570
|
-
: 0;
|
|
571
|
-
if (!durationMs) {
|
|
572
|
-
return null;
|
|
573
|
-
}
|
|
574
|
-
const mode = options.mode === 'blacklist' ? 'blacklist' : 'cooldown';
|
|
575
|
-
const nowMs = Date.now();
|
|
576
|
-
const previous = this.quotaStates.get(key) ??
|
|
577
|
-
createInitialQuotaState(key, this.staticConfigs.get(key), nowMs);
|
|
578
|
-
const next = mode === 'blacklist'
|
|
579
|
-
? {
|
|
580
|
-
...previous,
|
|
581
|
-
inPool: false,
|
|
582
|
-
reason: 'blacklist',
|
|
583
|
-
blacklistUntil: nowMs + durationMs,
|
|
584
|
-
cooldownUntil: null
|
|
585
|
-
}
|
|
586
|
-
: {
|
|
587
|
-
...previous,
|
|
588
|
-
inPool: false,
|
|
589
|
-
reason: 'cooldown',
|
|
590
|
-
cooldownUntil: nowMs + durationMs
|
|
591
|
-
};
|
|
592
|
-
this.quotaStates.set(key, next);
|
|
593
|
-
try {
|
|
594
|
-
await saveProviderQuotaSnapshot(this.toSnapshotObject(), new Date(nowMs));
|
|
595
|
-
}
|
|
596
|
-
catch {
|
|
597
|
-
// ignore persistence failure
|
|
598
|
-
}
|
|
599
|
-
return { providerKey: key, state: next };
|
|
600
|
-
}
|
|
601
|
-
getAdminSnapshot() {
|
|
602
|
-
return this.toSnapshotObject();
|
|
603
|
-
}
|
|
604
|
-
async start() {
|
|
605
|
-
if (!this.quotaRoutingEnabled) {
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
let center = null;
|
|
609
|
-
try {
|
|
610
|
-
center = await getProviderErrorCenter();
|
|
611
|
-
}
|
|
612
|
-
catch {
|
|
613
|
-
center = null;
|
|
614
|
-
}
|
|
615
|
-
if (center && typeof center.subscribe === 'function') {
|
|
616
|
-
this.unsubscribe = center.subscribe((event) => {
|
|
617
|
-
void this.handleProviderErrorEvent(event).catch(() => {
|
|
618
|
-
// swallow handler errors to avoid unhandled rejection noise; quota updates are best-effort
|
|
619
|
-
});
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
const intervalMs = readPositiveNumberFromEnv('ROUTECODEX_QUOTA_DAEMON_INTERVAL_MS', 60_000);
|
|
623
|
-
if (intervalMs > 0) {
|
|
624
|
-
this.maintenanceTimer = setInterval(() => {
|
|
625
|
-
void this.runMaintenanceTick().catch(() => {
|
|
626
|
-
// ignore maintenance failures
|
|
627
|
-
});
|
|
628
|
-
}, intervalMs);
|
|
629
|
-
}
|
|
630
|
-
// Run a one-off maintenance tick immediately so expired cooldown/blacklist entries
|
|
631
|
-
// are cleared even before the first request hits quotaView.
|
|
632
|
-
void this.runMaintenanceTick().catch(() => {
|
|
633
|
-
// ignore immediate tick failures
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
async stop() {
|
|
637
|
-
if (this.unsubscribe) {
|
|
638
|
-
try {
|
|
639
|
-
this.unsubscribe();
|
|
640
|
-
}
|
|
641
|
-
catch {
|
|
642
|
-
// ignore unsubscribe failures
|
|
643
|
-
}
|
|
644
|
-
this.unsubscribe = null;
|
|
645
|
-
}
|
|
646
|
-
if (this.maintenanceTimer) {
|
|
647
|
-
clearInterval(this.maintenanceTimer);
|
|
648
|
-
this.maintenanceTimer = null;
|
|
649
|
-
}
|
|
650
|
-
if (this.persistTimer) {
|
|
651
|
-
clearTimeout(this.persistTimer);
|
|
652
|
-
this.persistTimer = null;
|
|
653
|
-
}
|
|
654
|
-
if (!this.quotaRoutingEnabled) {
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
try {
|
|
658
|
-
await saveProviderQuotaSnapshot(this.toSnapshotObject(), new Date());
|
|
659
|
-
}
|
|
660
|
-
catch {
|
|
661
|
-
// best-effort persistence
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
recordProviderUsage(event) {
|
|
665
|
-
if (!this.quotaRoutingEnabled) {
|
|
666
|
-
return;
|
|
667
|
-
}
|
|
668
|
-
const providerKey = typeof event?.providerKey === 'string' ? event.providerKey.trim() : '';
|
|
669
|
-
if (!providerKey) {
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
672
|
-
const nowMs = typeof event.timestampMs === 'number' && Number.isFinite(event.timestampMs) && event.timestampMs > 0
|
|
673
|
-
? event.timestampMs
|
|
674
|
-
: Date.now();
|
|
675
|
-
const requestedTokens = typeof event.requestedTokens === 'number' && Number.isFinite(event.requestedTokens) && event.requestedTokens > 0
|
|
676
|
-
? event.requestedTokens
|
|
677
|
-
: 0;
|
|
678
|
-
const previous = this.quotaStates.get(providerKey) ??
|
|
679
|
-
createInitialQuotaState(providerKey, this.staticConfigs.get(providerKey), nowMs);
|
|
680
|
-
const nextState = applyQuotaUsageEvent(previous, { providerKey, requestedTokens, timestampMs: nowMs }, nowMs);
|
|
681
|
-
this.quotaStates.set(providerKey, nextState);
|
|
682
|
-
this.schedulePersist(nowMs);
|
|
683
|
-
}
|
|
684
|
-
recordProviderSuccess(event) {
|
|
685
|
-
if (!this.quotaRoutingEnabled) {
|
|
686
|
-
return;
|
|
687
|
-
}
|
|
688
|
-
const providerKey = typeof event?.providerKey === 'string' ? event.providerKey.trim() : '';
|
|
689
|
-
if (!providerKey) {
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
const nowMs = typeof event.timestampMs === 'number' && Number.isFinite(event.timestampMs) && event.timestampMs > 0
|
|
693
|
-
? event.timestampMs
|
|
694
|
-
: Date.now();
|
|
695
|
-
const usedTokens = typeof event.usedTokens === 'number' && Number.isFinite(event.usedTokens) && event.usedTokens > 0
|
|
696
|
-
? event.usedTokens
|
|
697
|
-
: 0;
|
|
698
|
-
const previous = this.quotaStates.get(providerKey) ??
|
|
699
|
-
createInitialQuotaState(providerKey, this.staticConfigs.get(providerKey), nowMs);
|
|
700
|
-
const nextState = applyQuotaSuccessEvent(previous, { providerKey, usedTokens, timestampMs: nowMs }, nowMs);
|
|
701
|
-
this.quotaStates.set(providerKey, nextState);
|
|
702
|
-
this.schedulePersist(nowMs);
|
|
703
|
-
}
|
|
704
|
-
registerProviderStaticConfig(providerKey, config = {}) {
|
|
705
|
-
if (!this.quotaRoutingEnabled) {
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
const key = typeof providerKey === 'string' ? providerKey.trim() : '';
|
|
709
|
-
if (!key) {
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
const authTypeRaw = typeof config.authType === 'string' ? config.authType.trim().toLowerCase() : '';
|
|
713
|
-
const authType = authTypeRaw === 'apikey' ? 'apikey' : authTypeRaw === 'oauth' ? 'oauth' : 'unknown';
|
|
714
|
-
const staticConfig = {
|
|
715
|
-
...(typeof config.priorityTier === 'number' && Number.isFinite(config.priorityTier)
|
|
716
|
-
? { priorityTier: config.priorityTier }
|
|
717
|
-
: {}),
|
|
718
|
-
authType
|
|
719
|
-
};
|
|
720
|
-
this.staticConfigs.set(key, staticConfig);
|
|
721
|
-
const nowMs = Date.now();
|
|
722
|
-
const existing = this.quotaStates.get(key);
|
|
723
|
-
if (existing) {
|
|
724
|
-
this.quotaStates.set(key, {
|
|
725
|
-
...existing,
|
|
726
|
-
authType,
|
|
727
|
-
...(typeof staticConfig.priorityTier === 'number' ? { priorityTier: staticConfig.priorityTier } : {})
|
|
728
|
-
});
|
|
729
|
-
return;
|
|
730
|
-
}
|
|
731
|
-
// If snapshot is missing (or operator cleared it), we still want a predictable default:
|
|
732
|
-
// API-key providers start "inPool: true" with unlimited quota until errors are observed.
|
|
733
|
-
// We persist (debounced) so admin tools can inspect the pool quickly after startup.
|
|
734
|
-
const initial = createInitialQuotaState(key, staticConfig, nowMs);
|
|
735
|
-
// Antigravity OAuth providers require an explicit quota snapshot before being routed:
|
|
736
|
-
// - If we haven't observed quota for a model yet, treat it as "no quota" and keep it out of pool.
|
|
737
|
-
// - QuotaManagerModule will emit QUOTA_RECOVERY / QUOTA_DEPLETED to flip this state.
|
|
738
|
-
// This prevents sticky/prefer targets from repeatedly hitting an untracked model and returning 429s.
|
|
739
|
-
const isAntigravity = key.toLowerCase().startsWith('antigravity.');
|
|
740
|
-
if (isAntigravity && authType === 'oauth') {
|
|
741
|
-
this.quotaStates.set(key, {
|
|
742
|
-
...initial,
|
|
743
|
-
inPool: false,
|
|
744
|
-
reason: 'cooldown',
|
|
745
|
-
cooldownUntil: null
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
else {
|
|
749
|
-
this.quotaStates.set(key, initial);
|
|
750
|
-
}
|
|
751
|
-
this.schedulePersist(nowMs);
|
|
752
|
-
}
|
|
753
|
-
async handleProviderErrorEvent(event) {
|
|
754
|
-
if (!event) {
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
const code = typeof event.code === 'string' ? event.code : '';
|
|
758
|
-
const providerKey = this.extractProviderKey(event);
|
|
759
|
-
if (!providerKey) {
|
|
760
|
-
return;
|
|
761
|
-
}
|
|
762
|
-
const nowMs = typeof event.timestamp === 'number' && Number.isFinite(event.timestamp) && event.timestamp > 0
|
|
763
|
-
? event.timestamp
|
|
764
|
-
: Date.now();
|
|
765
|
-
const previous = this.quotaStates.get(providerKey) ??
|
|
766
|
-
createInitialQuotaState(providerKey, this.staticConfigs.get(providerKey), nowMs);
|
|
767
|
-
// fatal 黑名单在锁定期内,不应被其它事件(包括 429/配额信号)改变。
|
|
768
|
-
if (previous.reason === 'fatal' && previous.blacklistUntil && nowMs < previous.blacklistUntil) {
|
|
769
|
-
return;
|
|
770
|
-
}
|
|
771
|
-
// QUOTA_* 属于“确定性配额信号”,不进入错误 series 统计。
|
|
772
|
-
if (code === 'QUOTA_DEPLETED') {
|
|
773
|
-
const detailCarrier = (event.details && typeof event.details === 'object') ? event.details : {};
|
|
774
|
-
const raw = detailCarrier.virtualRouterQuotaDepleted;
|
|
775
|
-
const cooldownMs = raw && typeof raw === 'object' && typeof raw.cooldownMs === 'number'
|
|
776
|
-
? raw.cooldownMs
|
|
777
|
-
: undefined;
|
|
778
|
-
const ttl = typeof cooldownMs === 'number' && Number.isFinite(cooldownMs) && cooldownMs > 0
|
|
779
|
-
? cooldownMs
|
|
780
|
-
: undefined;
|
|
781
|
-
const nextState = {
|
|
782
|
-
...previous,
|
|
783
|
-
inPool: false,
|
|
784
|
-
reason: 'quotaDepleted',
|
|
785
|
-
cooldownUntil: ttl ? nowMs + ttl : previous.cooldownUntil
|
|
786
|
-
};
|
|
787
|
-
this.quotaStates.set(providerKey, nextState);
|
|
788
|
-
this.schedulePersist(nowMs);
|
|
789
|
-
return;
|
|
790
|
-
}
|
|
791
|
-
if (code === 'QUOTA_RECOVERY') {
|
|
792
|
-
const withinBlacklist = previous.blacklistUntil !== null && nowMs < previous.blacklistUntil;
|
|
793
|
-
const withinFatalBlacklist = previous.reason === 'fatal' && previous.blacklistUntil !== null && nowMs < previous.blacklistUntil;
|
|
794
|
-
if (!withinBlacklist && !withinFatalBlacklist) {
|
|
795
|
-
const nextState = {
|
|
796
|
-
...previous,
|
|
797
|
-
inPool: true,
|
|
798
|
-
reason: 'ok',
|
|
799
|
-
cooldownUntil: null
|
|
800
|
-
};
|
|
801
|
-
this.quotaStates.set(providerKey, nextState);
|
|
802
|
-
this.schedulePersist(nowMs);
|
|
803
|
-
}
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
|
-
// Gemini-family quota exhausted errors often carry quota reset delay.
|
|
807
|
-
// When present, treat as deterministic quota depletion signal rather than generic 429 backoff/blacklist.
|
|
808
|
-
if (typeof event.status === 'number' && event.status === 429) {
|
|
809
|
-
const seriesCooldownUntil = extractVirtualRouterSeriesCooldownUntil(event, nowMs);
|
|
810
|
-
if (seriesCooldownUntil) {
|
|
811
|
-
const nextState = {
|
|
812
|
-
...previous,
|
|
813
|
-
inPool: false,
|
|
814
|
-
reason: 'quotaDepleted',
|
|
815
|
-
cooldownUntil: seriesCooldownUntil,
|
|
816
|
-
blacklistUntil: null,
|
|
817
|
-
lastErrorSeries: null,
|
|
818
|
-
consecutiveErrorCount: 0
|
|
819
|
-
};
|
|
820
|
-
this.quotaStates.set(providerKey, nextState);
|
|
821
|
-
this.schedulePersist(nowMs);
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
824
|
-
const runtime = event.runtime;
|
|
825
|
-
const providerIdRaw = runtime && typeof runtime.providerId === 'string' ? runtime.providerId.trim().toLowerCase() : '';
|
|
826
|
-
const isQuotaProvider = providerIdRaw === 'antigravity' || providerIdRaw === 'gemini-cli';
|
|
827
|
-
if (isQuotaProvider) {
|
|
828
|
-
const ttl = parseQuotaResetDelayMs(event);
|
|
829
|
-
if (ttl && ttl > 0) {
|
|
830
|
-
const nextState = {
|
|
831
|
-
...previous,
|
|
832
|
-
inPool: false,
|
|
833
|
-
reason: 'quotaDepleted',
|
|
834
|
-
cooldownUntil: nowMs + ttl,
|
|
835
|
-
blacklistUntil: null,
|
|
836
|
-
lastErrorSeries: null,
|
|
837
|
-
consecutiveErrorCount: 0
|
|
838
|
-
};
|
|
839
|
-
this.quotaStates.set(providerKey, nextState);
|
|
840
|
-
this.schedulePersist(nowMs);
|
|
841
|
-
return;
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
const errorForQuota = {
|
|
846
|
-
providerKey,
|
|
847
|
-
httpStatus: typeof event.status === 'number' ? event.status : undefined,
|
|
848
|
-
code: typeof event.code === 'string' ? event.code : undefined,
|
|
849
|
-
fatal: this.isFatalForQuota(event),
|
|
850
|
-
timestampMs: nowMs
|
|
851
|
-
};
|
|
852
|
-
const nextState = applyQuotaErrorEvent(previous, errorForQuota, nowMs);
|
|
853
|
-
this.quotaStates.set(providerKey, nextState);
|
|
854
|
-
const tsIso = new Date(nowMs).toISOString();
|
|
855
|
-
try {
|
|
856
|
-
await appendProviderErrorEvent({
|
|
857
|
-
ts: tsIso,
|
|
858
|
-
providerKey,
|
|
859
|
-
code: typeof errorForQuota.code === 'string' ? errorForQuota.code : undefined,
|
|
860
|
-
httpStatus: typeof errorForQuota.httpStatus === 'number' ? errorForQuota.httpStatus : undefined,
|
|
861
|
-
message: event.message,
|
|
862
|
-
details: {
|
|
863
|
-
stage: event.stage,
|
|
864
|
-
routeName: event.runtime.routeName,
|
|
865
|
-
entryEndpoint: event.runtime.entryEndpoint
|
|
866
|
-
}
|
|
867
|
-
});
|
|
868
|
-
}
|
|
869
|
-
catch {
|
|
870
|
-
// logging failure is non-fatal
|
|
871
|
-
}
|
|
872
|
-
try {
|
|
873
|
-
await saveProviderQuotaSnapshot(this.toSnapshotObject(), new Date(nowMs));
|
|
874
|
-
}
|
|
875
|
-
catch {
|
|
876
|
-
// best-effort persistence only
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
async runMaintenanceTick() {
|
|
880
|
-
if (!this.quotaStates.size) {
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
883
|
-
const nowMs = Date.now();
|
|
884
|
-
const updated = new Map();
|
|
885
|
-
for (const [providerKey, state] of this.quotaStates.entries()) {
|
|
886
|
-
const next = tickQuotaStateTime(state, nowMs);
|
|
887
|
-
updated.set(providerKey, next);
|
|
888
|
-
}
|
|
889
|
-
this.quotaStates = updated;
|
|
890
|
-
try {
|
|
891
|
-
await saveProviderQuotaSnapshot(this.toSnapshotObject(), new Date(nowMs));
|
|
892
|
-
}
|
|
893
|
-
catch {
|
|
894
|
-
// ignore persistence errors
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
schedulePersist(_nowMs) {
|
|
898
|
-
if (this.persistTimer) {
|
|
899
|
-
return;
|
|
900
|
-
}
|
|
901
|
-
const debounceMs = readPositiveNumberFromEnv('ROUTECODEX_QUOTA_PERSIST_DEBOUNCE_MS', 5_000);
|
|
902
|
-
this.persistTimer = setTimeout(() => {
|
|
903
|
-
this.persistTimer = null;
|
|
904
|
-
void saveProviderQuotaSnapshot(this.toSnapshotObject(), new Date()).catch(() => {
|
|
905
|
-
// ignore persistence errors
|
|
906
|
-
});
|
|
907
|
-
}, debounceMs);
|
|
908
|
-
}
|
|
909
|
-
toSnapshotObject() {
|
|
910
|
-
const result = {};
|
|
911
|
-
for (const [key, state] of this.quotaStates.entries()) {
|
|
912
|
-
result[key] = state;
|
|
913
|
-
}
|
|
914
|
-
return result;
|
|
915
|
-
}
|
|
916
|
-
extractProviderKey(event) {
|
|
917
|
-
const runtime = event.runtime;
|
|
918
|
-
const direct = runtime && typeof runtime.providerKey === 'string' && runtime.providerKey.trim()
|
|
919
|
-
? runtime.providerKey.trim()
|
|
920
|
-
: null;
|
|
921
|
-
if (direct) {
|
|
922
|
-
return direct;
|
|
923
|
-
}
|
|
924
|
-
const target = runtime && runtime.target;
|
|
925
|
-
if (target && typeof target === 'object') {
|
|
926
|
-
const targetKey = target.providerKey;
|
|
927
|
-
if (typeof targetKey === 'string' && targetKey.trim()) {
|
|
928
|
-
return targetKey.trim();
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
return null;
|
|
932
|
-
}
|
|
933
|
-
getQuotaView() {
|
|
934
|
-
if (!this.quotaRoutingEnabled) {
|
|
935
|
-
return () => null;
|
|
936
|
-
}
|
|
937
|
-
return (providerKey) => {
|
|
938
|
-
const key = typeof providerKey === 'string' ? providerKey.trim() : '';
|
|
939
|
-
if (!key) {
|
|
940
|
-
return null;
|
|
941
|
-
}
|
|
942
|
-
const state = this.quotaStates.get(key);
|
|
943
|
-
if (!state) {
|
|
944
|
-
return null;
|
|
945
|
-
}
|
|
946
|
-
// 视图层做一次“即时修复”,确保即使 maintenance tick 未运行,
|
|
947
|
-
// 冷却/黑名单到期也能立刻恢复可用状态,避免路由长期卡死。
|
|
948
|
-
const nowMs = Date.now();
|
|
949
|
-
const normalized = tickQuotaStateTime(state, nowMs);
|
|
950
|
-
if (normalized !== state) {
|
|
951
|
-
this.quotaStates.set(key, normalized);
|
|
952
|
-
this.schedulePersist(nowMs);
|
|
953
|
-
}
|
|
954
|
-
const effective = normalized;
|
|
955
|
-
return {
|
|
956
|
-
providerKey: effective.providerKey,
|
|
957
|
-
inPool: effective.inPool,
|
|
958
|
-
reason: effective.reason,
|
|
959
|
-
priorityTier: effective.priorityTier,
|
|
960
|
-
cooldownUntil: effective.cooldownUntil ?? null,
|
|
961
|
-
blacklistUntil: effective.blacklistUntil ?? null
|
|
962
|
-
};
|
|
963
|
-
};
|
|
964
|
-
}
|
|
965
|
-
isFatalForQuota(event) {
|
|
966
|
-
const status = typeof event.status === 'number' ? event.status : undefined;
|
|
967
|
-
const code = typeof event.code === 'string' ? event.code.toUpperCase() : '';
|
|
968
|
-
const stage = typeof event.stage === 'string' ? event.stage.toLowerCase() : '';
|
|
969
|
-
if (status === 401 || status === 402 || status === 403) {
|
|
970
|
-
return true;
|
|
971
|
-
}
|
|
972
|
-
if (code.includes('AUTH') || code.includes('UNAUTHORIZED')) {
|
|
973
|
-
return true;
|
|
974
|
-
}
|
|
975
|
-
if (code.includes('CONFIG')) {
|
|
976
|
-
return true;
|
|
977
|
-
}
|
|
978
|
-
if (stage.includes('compat')) {
|
|
979
|
-
return true;
|
|
980
|
-
}
|
|
981
|
-
if (event.recoverable === false && status !== undefined && status >= 500) {
|
|
982
|
-
return true;
|
|
983
|
-
}
|
|
984
|
-
return false;
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
function extractVirtualRouterSeriesCooldownUntil(event, nowMs) {
|
|
988
|
-
if (!event || !event.details || typeof event.details !== 'object') {
|
|
989
|
-
return null;
|
|
990
|
-
}
|
|
991
|
-
const raw = event.details.virtualRouterSeriesCooldown;
|
|
992
|
-
if (!raw || typeof raw !== 'object') {
|
|
993
|
-
return null;
|
|
994
|
-
}
|
|
995
|
-
const record = raw;
|
|
996
|
-
const cooldownMsRaw = record.cooldownMs;
|
|
997
|
-
const expiresAtRaw = record.expiresAt;
|
|
998
|
-
const expiresAt = typeof expiresAtRaw === 'number' && Number.isFinite(expiresAtRaw) && expiresAtRaw > nowMs
|
|
999
|
-
? expiresAtRaw
|
|
1000
|
-
: null;
|
|
1001
|
-
if (expiresAt) {
|
|
1002
|
-
return expiresAt;
|
|
1003
|
-
}
|
|
1004
|
-
const cooldownMs = typeof cooldownMsRaw === 'number'
|
|
1005
|
-
? cooldownMsRaw
|
|
1006
|
-
: typeof cooldownMsRaw === 'string'
|
|
1007
|
-
? Number.parseFloat(cooldownMsRaw)
|
|
1008
|
-
: Number.NaN;
|
|
1009
|
-
if (!Number.isFinite(cooldownMs) || cooldownMs <= 0) {
|
|
1010
|
-
return null;
|
|
1011
|
-
}
|
|
1012
|
-
return nowMs + cooldownMs;
|
|
1013
|
-
}
|
|
1014
|
-
function parseQuotaResetDelayMs(event) {
|
|
1015
|
-
const message = typeof event.message === 'string' ? event.message : '';
|
|
1016
|
-
const raw = message.toLowerCase();
|
|
1017
|
-
// Common shape: "reset after 3h22m41s" (Gemini quota exhausted)
|
|
1018
|
-
const afterMatch = raw.match(/reset after\s+([0-9a-z.\s]+)\.?/i);
|
|
1019
|
-
if (afterMatch && afterMatch[1]) {
|
|
1020
|
-
const parsed = parseDurationToMs(afterMatch[1]);
|
|
1021
|
-
if (parsed && parsed > 0) {
|
|
1022
|
-
return parsed;
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
// Sometimes the upstream JSON is embedded; try extracting quotaResetDelay.
|
|
1026
|
-
const embeddedDelayMatch = raw.match(/quotaresetdelay"\s*:\s*"([^"]+)"/i);
|
|
1027
|
-
if (embeddedDelayMatch && embeddedDelayMatch[1]) {
|
|
1028
|
-
const parsed = parseDurationToMs(embeddedDelayMatch[1]);
|
|
1029
|
-
if (parsed && parsed > 0) {
|
|
1030
|
-
return parsed;
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
return null;
|
|
1034
|
-
}
|
|
1035
|
-
function parseDurationToMs(value) {
|
|
1036
|
-
if (!value || typeof value !== 'string') {
|
|
1037
|
-
return null;
|
|
1038
|
-
}
|
|
1039
|
-
const pattern = /(\d+(?:\.\d+)?)(ms|s|m|h)/gi;
|
|
1040
|
-
let totalMs = 0;
|
|
1041
|
-
let matched = false;
|
|
1042
|
-
let match;
|
|
1043
|
-
while ((match = pattern.exec(value)) !== null) {
|
|
1044
|
-
matched = true;
|
|
1045
|
-
const amount = Number.parseFloat(match[1]);
|
|
1046
|
-
if (!Number.isFinite(amount)) {
|
|
1047
|
-
continue;
|
|
1048
|
-
}
|
|
1049
|
-
const unit = match[2].toLowerCase();
|
|
1050
|
-
if (unit === 'ms') {
|
|
1051
|
-
totalMs += amount;
|
|
1052
|
-
}
|
|
1053
|
-
else if (unit === 'h') {
|
|
1054
|
-
totalMs += amount * 3_600_000;
|
|
1055
|
-
}
|
|
1056
|
-
else if (unit === 'm') {
|
|
1057
|
-
totalMs += amount * 60_000;
|
|
1058
|
-
}
|
|
1059
|
-
else if (unit === 's') {
|
|
1060
|
-
totalMs += amount * 1_000;
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
if (!matched) {
|
|
1064
|
-
return null;
|
|
1065
|
-
}
|
|
1066
|
-
if (totalMs <= 0) {
|
|
1067
|
-
return null;
|
|
1068
|
-
}
|
|
1069
|
-
return Math.round(totalMs);
|
|
1070
|
-
}
|
|
1071
|
-
function normalizeLoadedQuotaState(providerKey, state) {
|
|
1072
|
-
const key = typeof providerKey === 'string' && providerKey.trim() ? providerKey.trim() : state.providerKey;
|
|
1073
|
-
const rawAuth = typeof state.authType === 'string'
|
|
1074
|
-
? String(state.authType).trim().toLowerCase()
|
|
1075
|
-
: '';
|
|
1076
|
-
const authType = rawAuth === 'apikey' ? 'apikey' : rawAuth === 'oauth' ? 'oauth' : 'unknown';
|
|
1077
|
-
return {
|
|
1078
|
-
...state,
|
|
1079
|
-
providerKey: key,
|
|
1080
|
-
authType
|
|
1081
|
-
};
|
|
1082
|
-
}
|
|
1083
|
-
function readPositiveNumberFromEnv(name, fallback) {
|
|
1084
|
-
const raw = process.env[name];
|
|
1085
|
-
if (!raw) {
|
|
1086
|
-
return fallback;
|
|
1087
|
-
}
|
|
1088
|
-
const parsed = Number(raw);
|
|
1089
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
1090
|
-
return fallback;
|
|
1091
|
-
}
|
|
1092
|
-
return parsed;
|
|
1093
|
-
}
|
|
1
|
+
export { QuotaManagerModule } from './antigravity-quota-manager.js';
|
|
2
|
+
export { ProviderQuotaDaemonModule } from './provider-quota-daemon.js';
|
|
1094
3
|
//# sourceMappingURL=index.js.map
|