@prometheus-ai/ai 0.5.4 → 0.5.8
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/dist/types/auth-broker/remote-store.d.ts +2 -1
- package/dist/types/auth-broker/wire-schemas.d.ts +4 -1
- package/dist/types/auth-gateway/server.d.ts +19 -0
- package/dist/types/auth-gateway/types.d.ts +9 -3
- package/dist/types/auth-retry.d.ts +119 -0
- package/dist/types/auth-storage.d.ts +217 -8
- package/dist/types/errors.d.ts +24 -0
- package/dist/types/index.d.ts +5 -9
- package/dist/types/provider-details.d.ts +1 -1
- package/dist/types/providers/amazon-bedrock.d.ts +12 -6
- package/dist/types/providers/anthropic-client.d.ts +10 -3
- package/dist/types/providers/anthropic-messages-server-schema.d.ts +2 -2
- package/dist/types/providers/anthropic-messages-server.d.ts +3 -3
- package/dist/types/providers/anthropic-wire.d.ts +3 -3
- package/dist/types/providers/anthropic.d.ts +41 -34
- package/dist/types/providers/aws-credentials.d.ts +8 -0
- package/dist/types/providers/azure-openai-responses.d.ts +1 -0
- package/dist/types/providers/google-gemini-cli.d.ts +22 -1
- package/dist/types/providers/google-shared.d.ts +22 -0
- package/dist/types/providers/google-types.d.ts +13 -1
- package/dist/types/providers/mock.d.ts +8 -3
- package/dist/types/providers/ollama.d.ts +6 -0
- package/dist/types/providers/openai-chat-server-schema.d.ts +6 -3
- package/dist/types/providers/openai-chat-server.d.ts +3 -3
- package/dist/types/providers/openai-chat-wire.d.ts +644 -0
- package/dist/types/providers/openai-codex/request-transformer.d.ts +8 -0
- package/dist/types/providers/openai-codex/response-handler.d.ts +9 -0
- package/dist/types/providers/openai-codex-responses.d.ts +31 -2
- package/dist/types/providers/openai-completions-compat.d.ts +2 -25
- package/dist/types/providers/openai-completions.d.ts +2 -10
- package/dist/types/providers/openai-responses-server-schema.d.ts +4 -4
- package/dist/types/providers/openai-responses-server.d.ts +2 -2
- package/dist/types/providers/openai-responses-shared.d.ts +49 -9
- package/dist/types/providers/openai-responses-wire.d.ts +6065 -0
- package/dist/types/providers/openai-responses.d.ts +13 -4
- package/dist/types/providers/prometheus-native-client.d.ts +9 -0
- package/dist/types/providers/prometheus-native-server.d.ts +4 -3
- package/dist/types/providers/transform-messages.d.ts +1 -2
- package/dist/types/rate-limit-utils.d.ts +3 -2
- package/dist/types/registry/aimlapi.d.ts +4 -0
- package/dist/types/registry/alibaba-coding-plan.d.ts +7 -0
- package/dist/types/registry/amazon-bedrock.d.ts +5 -0
- package/dist/types/registry/anthropic.d.ts +10 -0
- package/dist/types/{utils/oauth → registry}/api-key-login.d.ts +8 -2
- package/dist/types/{utils/oauth → registry}/api-key-validation.d.ts +15 -0
- package/dist/types/registry/cerebras.d.ts +7 -0
- package/dist/types/registry/cloudflare-ai-gateway.d.ts +13 -0
- package/dist/types/registry/cursor.d.ts +7 -0
- package/dist/types/registry/deepseek.d.ts +8 -0
- package/dist/types/registry/derived.d.ts +5 -0
- package/dist/types/registry/firepass.d.ts +16 -0
- package/dist/types/registry/fireworks.d.ts +7 -0
- package/dist/types/registry/github-copilot.d.ts +7 -0
- package/dist/types/registry/gitlab-duo.d.ts +9 -0
- package/dist/types/registry/google-antigravity.d.ts +9 -0
- package/dist/types/registry/google-gemini-cli.d.ts +9 -0
- package/dist/types/registry/google-vertex.d.ts +5 -0
- package/dist/types/registry/google.d.ts +4 -0
- package/dist/types/registry/groq.d.ts +4 -0
- package/dist/types/registry/huggingface.d.ts +7 -0
- package/dist/types/registry/index.d.ts +4 -0
- package/dist/types/registry/kagi.d.ts +14 -0
- package/dist/types/registry/kilo.d.ts +7 -0
- package/dist/types/registry/kimi-code.d.ts +7 -0
- package/dist/types/registry/litellm.d.ts +13 -0
- package/dist/types/registry/lm-studio.d.ts +8 -0
- package/dist/types/registry/minimax-code-cn.d.ts +6 -0
- package/dist/types/registry/minimax-code.d.ts +6 -0
- package/dist/types/registry/minimax.d.ts +4 -0
- package/dist/types/registry/mistral.d.ts +4 -0
- package/dist/types/registry/moonshot.d.ts +7 -0
- package/dist/types/registry/nanogpt.d.ts +7 -0
- package/dist/types/registry/nvidia.d.ts +7 -0
- package/dist/types/registry/oauth/__tests__/xai-oauth.test.d.ts +1 -0
- package/dist/types/{utils → registry}/oauth/anthropic.d.ts +2 -1
- package/dist/types/{utils → registry}/oauth/github-copilot.d.ts +15 -23
- package/dist/types/{utils → registry}/oauth/index.d.ts +1 -0
- package/dist/types/{utils → registry}/oauth/minimax-code.d.ts +5 -5
- package/dist/types/{utils → registry}/oauth/types.d.ts +6 -1
- package/dist/types/{utils → registry}/oauth/xai-oauth.d.ts +2 -1
- package/dist/types/registry/ollama-cloud.d.ts +7 -0
- package/dist/types/registry/ollama.d.ts +12 -0
- package/dist/types/registry/openai-codex-device.d.ts +8 -0
- package/dist/types/registry/openai-codex.d.ts +9 -0
- package/dist/types/registry/openai.d.ts +4 -0
- package/dist/types/registry/opencode-go.d.ts +6 -0
- package/dist/types/registry/opencode-zen.d.ts +6 -0
- package/dist/types/registry/openrouter.d.ts +13 -0
- package/dist/types/registry/parallel.d.ts +14 -0
- package/dist/types/registry/perplexity.d.ts +7 -0
- package/dist/types/registry/qianfan.d.ts +7 -0
- package/dist/types/registry/qwen-portal.d.ts +7 -0
- package/dist/types/registry/registry.d.ts +272 -0
- package/dist/types/registry/synthetic.d.ts +6 -0
- package/dist/types/registry/tavily.d.ts +14 -0
- package/dist/types/registry/together.d.ts +6 -0
- package/dist/types/registry/types.d.ts +51 -0
- package/dist/types/registry/venice.d.ts +13 -0
- package/dist/types/registry/vercel-ai-gateway.d.ts +7 -0
- package/dist/types/registry/vllm.d.ts +7 -0
- package/dist/types/registry/wafer-pass.d.ts +6 -0
- package/dist/types/registry/wafer-serverless.d.ts +6 -0
- package/dist/types/registry/xai-oauth.d.ts +7 -0
- package/dist/types/registry/xai.d.ts +4 -0
- package/dist/types/registry/xiaomi-token-plan-ams.d.ts +6 -0
- package/dist/types/registry/xiaomi-token-plan-cn.d.ts +6 -0
- package/dist/types/registry/xiaomi-token-plan-sgp.d.ts +6 -0
- package/dist/types/registry/xiaomi.d.ts +6 -0
- package/dist/types/registry/zai.d.ts +7 -0
- package/dist/types/registry/zenmux.d.ts +7 -0
- package/dist/types/registry/zhipu-coding-plan.d.ts +7 -0
- package/dist/types/stream.d.ts +9 -1
- package/dist/types/types.d.ts +56 -295
- package/dist/types/usage/google-antigravity.d.ts +15 -1
- package/dist/types/usage/openai-codex-reset.d.ts +79 -0
- package/dist/types/usage/openai-codex.d.ts +1 -0
- package/dist/types/usage.d.ts +77 -4
- package/dist/types/utils/abort.d.ts +6 -0
- package/dist/types/utils/event-stream.d.ts +2 -0
- package/dist/types/utils/http-inspector.d.ts +0 -1
- package/dist/types/utils/idle-iterator.d.ts +35 -0
- package/dist/types/utils/openai-http.d.ts +58 -0
- package/dist/types/utils/request-debug.d.ts +3 -0
- package/dist/types/utils/retry-after.d.ts +1 -0
- package/dist/types/utils/schema/fields.d.ts +5 -0
- package/dist/types/utils/schema/json-schema-validator.d.ts +8 -0
- package/dist/types/utils/schema/stamps.d.ts +7 -15
- package/dist/types/utils/sse-debug.d.ts +0 -5
- package/dist/types/utils/stream-markup-healing.d.ts +2 -0
- package/dist/types/utils.d.ts +1 -5
- package/package.json +17 -29
- package/src/auth-broker/remote-store.ts +10 -1
- package/src/auth-broker/snapshot-cache.ts +1 -1
- package/src/auth-broker/wire-schemas.ts +1 -1
- package/src/auth-gateway/http.ts +1 -1
- package/src/auth-gateway/server.ts +95 -30
- package/src/auth-gateway/types.ts +10 -2
- package/src/auth-retry.ts +238 -0
- package/src/auth-storage.ts +935 -430
- package/src/errors.ts +32 -0
- package/src/index.ts +9 -14
- package/src/provider-details.ts +1 -1
- package/src/providers/__tests__/google-auth.test.ts +144 -0
- package/src/providers/amazon-bedrock.ts +70 -40
- package/src/providers/anthropic-client.ts +15 -13
- package/src/providers/anthropic-messages-server-schema.ts +17 -7
- package/src/providers/anthropic-messages-server.ts +88 -20
- package/src/providers/anthropic-wire.ts +4 -3
- package/src/providers/anthropic.ts +1234 -621
- package/src/providers/aws-credentials.ts +47 -5
- package/src/providers/aws-eventstream.ts +5 -0
- package/src/providers/azure-openai-responses.ts +117 -67
- package/src/providers/cursor.ts +30 -30
- package/src/providers/github-copilot-headers.ts +1 -1
- package/src/providers/gitlab-duo.ts +36 -29
- package/src/providers/google-auth.ts +71 -8
- package/src/providers/google-gemini-cli.ts +118 -22
- package/src/providers/google-shared.ts +163 -43
- package/src/providers/google-types.ts +10 -1
- package/src/providers/kimi.ts +1 -1
- package/src/providers/mock.ts +11 -3
- package/src/providers/ollama.ts +64 -7
- package/src/providers/openai-anthropic-shim.ts +17 -8
- package/src/providers/openai-chat-server-schema.ts +9 -3
- package/src/providers/openai-chat-server.ts +82 -16
- package/src/providers/openai-chat-wire.ts +847 -0
- package/src/providers/openai-codex/request-transformer.ts +129 -34
- package/src/providers/openai-codex/response-handler.ts +22 -1
- package/src/providers/openai-codex-responses.ts +699 -247
- package/src/providers/openai-completions-compat.ts +8 -308
- package/src/providers/openai-completions.ts +416 -267
- package/src/providers/openai-responses-server-schema.ts +15 -9
- package/src/providers/openai-responses-server.ts +162 -114
- package/src/providers/openai-responses-shared.ts +320 -82
- package/src/providers/openai-responses-wire.ts +6391 -0
- package/src/providers/openai-responses.ts +382 -176
- package/src/providers/prometheus-native-client.ts +27 -11
- package/src/providers/prometheus-native-server.ts +44 -17
- package/src/providers/transform-messages.ts +311 -120
- package/src/providers/vision-guard.ts +5 -3
- package/src/rate-limit-utils.ts +13 -3
- package/src/registry/aimlapi.ts +6 -0
- package/src/{utils/oauth → registry}/alibaba-coding-plan.ts +8 -18
- package/src/registry/amazon-bedrock.ts +22 -0
- package/src/registry/anthropic.ts +26 -0
- package/src/{utils/oauth → registry}/api-key-login.ts +25 -3
- package/src/{utils/oauth → registry}/api-key-validation.ts +62 -2
- package/src/{utils/oauth → registry}/cerebras.ts +8 -1
- package/src/{utils/oauth → registry}/cloudflare-ai-gateway.ts +8 -12
- package/src/registry/cursor.ts +20 -0
- package/src/{utils/oauth → registry}/deepseek.ts +9 -17
- package/src/registry/derived.ts +9 -0
- package/src/{utils/oauth → registry}/firepass.ts +10 -2
- package/src/{utils/oauth → registry}/fireworks.ts +8 -1
- package/src/registry/github-copilot.ts +22 -0
- package/src/registry/gitlab-duo.ts +19 -0
- package/src/registry/google-antigravity.ts +21 -0
- package/src/registry/google-gemini-cli.ts +21 -0
- package/src/registry/google-vertex.ts +38 -0
- package/src/registry/google.ts +6 -0
- package/src/registry/groq.ts +6 -0
- package/src/{utils/oauth → registry}/huggingface.ts +8 -19
- package/src/registry/index.ts +4 -0
- package/src/{utils/oauth → registry}/kagi.ts +9 -11
- package/src/{utils/oauth → registry}/kilo.ts +11 -6
- package/src/registry/kimi-code.ts +17 -0
- package/src/{utils/oauth → registry}/litellm.ts +8 -12
- package/src/{utils/oauth → registry}/lm-studio.ts +9 -17
- package/src/registry/minimax-code-cn.ts +12 -0
- package/src/registry/minimax-code.ts +12 -0
- package/src/registry/minimax.ts +6 -0
- package/src/registry/mistral.ts +6 -0
- package/src/{utils/oauth → registry}/moonshot.ts +8 -9
- package/src/{utils/oauth → registry}/nanogpt.ts +8 -1
- package/src/{utils/oauth → registry}/nvidia.ts +8 -18
- package/src/{utils → registry}/oauth/__tests__/xai-oauth.test.ts +4 -7
- package/src/{utils → registry}/oauth/anthropic.ts +38 -17
- package/src/{utils → registry}/oauth/github-copilot.ts +79 -115
- package/src/registry/oauth/gitlab-duo.ts +198 -0
- package/src/{utils → registry}/oauth/google-antigravity.ts +1 -4
- package/src/{utils → registry}/oauth/google-gemini-cli.ts +1 -4
- package/src/registry/oauth/index.ts +164 -0
- package/src/{utils → registry}/oauth/minimax-code.ts +16 -14
- package/src/{utils → registry}/oauth/types.ts +7 -51
- package/src/{utils → registry}/oauth/wafer.ts +1 -1
- package/src/{utils → registry}/oauth/xai-oauth.ts +16 -8
- package/src/{utils → registry}/oauth/xiaomi.ts +9 -4
- package/src/{utils/oauth → registry}/ollama-cloud.ts +8 -1
- package/src/{utils/oauth → registry}/ollama.ts +8 -13
- package/src/registry/openai-codex-device.ts +18 -0
- package/src/registry/openai-codex.ts +19 -0
- package/src/registry/openai.ts +6 -0
- package/src/registry/opencode-go.ts +12 -0
- package/src/registry/opencode-zen.ts +12 -0
- package/src/{utils/oauth → registry}/openrouter.ts +10 -2
- package/src/{utils/oauth → registry}/parallel.ts +9 -11
- package/src/registry/perplexity.ts +13 -0
- package/src/{utils/oauth → registry}/qianfan.ts +8 -17
- package/src/{utils/oauth → registry}/qwen-portal.ts +8 -19
- package/src/registry/registry.ts +149 -0
- package/src/{utils/oauth → registry}/synthetic.ts +7 -1
- package/src/{utils/oauth → registry}/tavily.ts +10 -12
- package/src/{utils/oauth → registry}/together.ts +7 -1
- package/src/registry/types.ts +56 -0
- package/src/{utils/oauth → registry}/venice.ts +8 -12
- package/src/{utils/oauth → registry}/vercel-ai-gateway.ts +8 -18
- package/src/{utils/oauth → registry}/vllm.ts +9 -16
- package/src/registry/wafer-pass.ts +12 -0
- package/src/registry/wafer-serverless.ts +12 -0
- package/src/registry/xai-oauth.ts +17 -0
- package/src/registry/xai.ts +6 -0
- package/src/registry/xiaomi-token-plan-ams.ts +12 -0
- package/src/registry/xiaomi-token-plan-cn.ts +12 -0
- package/src/registry/xiaomi-token-plan-sgp.ts +12 -0
- package/src/registry/xiaomi.ts +12 -0
- package/src/{utils/oauth → registry}/zai.ts +10 -22
- package/src/{utils/oauth → registry}/zenmux.ts +8 -1
- package/src/{utils/oauth/zhipu.ts → registry/zhipu-coding-plan.ts} +9 -21
- package/src/stream.ts +229 -199
- package/src/types.ts +63 -384
- package/src/usage/claude.ts +4 -2
- package/src/usage/github-copilot.ts +4 -2
- package/src/usage/google-antigravity.ts +196 -28
- package/src/usage/kimi.ts +1 -1
- package/src/usage/minimax-code.ts +5 -6
- package/src/usage/openai-codex-reset.ts +174 -0
- package/src/usage/openai-codex.ts +19 -2
- package/src/usage/zai.ts +2 -1
- package/src/usage.ts +93 -4
- package/src/utils/abort.ts +14 -0
- package/src/utils/event-stream.ts +17 -0
- package/src/utils/http-inspector.ts +4 -12
- package/src/utils/idle-iterator.ts +250 -79
- package/src/utils/openai-http.ts +157 -0
- package/src/utils/request-debug.ts +67 -19
- package/src/utils/retry-after.ts +1 -1
- package/src/utils/retry.ts +23 -2
- package/src/utils/schema/CONSTRAINTS.md +4 -2
- package/src/utils/schema/fields.ts +16 -0
- package/src/utils/schema/json-schema-validator.ts +19 -1
- package/src/utils/schema/normalize.ts +80 -8
- package/src/utils/schema/stamps.ts +22 -10
- package/src/utils/schema/wire.ts +2 -2
- package/src/utils/sse-debug.ts +0 -271
- package/src/utils/stream-markup-healing.ts +50 -8
- package/src/utils/validation.ts +49 -13
- package/src/utils.ts +2 -26
- package/dist/types/model-cache.d.ts +0 -17
- package/dist/types/model-manager.d.ts +0 -64
- package/dist/types/model-thinking.d.ts +0 -100
- package/dist/types/models.d.ts +0 -12
- package/dist/types/provider-models/bundled-references.d.ts +0 -4
- package/dist/types/provider-models/descriptors.d.ts +0 -50
- package/dist/types/provider-models/google.d.ts +0 -24
- package/dist/types/provider-models/index.d.ts +0 -5
- package/dist/types/provider-models/ollama.d.ts +0 -7
- package/dist/types/provider-models/openai-compat.d.ts +0 -323
- package/dist/types/provider-models/special.d.ts +0 -16
- package/dist/types/utils/discovery/antigravity.d.ts +0 -61
- package/dist/types/utils/discovery/codex.d.ts +0 -38
- package/dist/types/utils/discovery/cursor.d.ts +0 -23
- package/dist/types/utils/discovery/gemini.d.ts +0 -25
- package/dist/types/utils/discovery/index.d.ts +0 -4
- package/dist/types/utils/discovery/openai-compatible.d.ts +0 -72
- package/dist/types/utils/oauth/alibaba-coding-plan.d.ts +0 -18
- package/dist/types/utils/oauth/cerebras.d.ts +0 -1
- package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +0 -18
- package/dist/types/utils/oauth/deepseek.d.ts +0 -10
- package/dist/types/utils/oauth/firepass.d.ts +0 -1
- package/dist/types/utils/oauth/fireworks.d.ts +0 -1
- package/dist/types/utils/oauth/huggingface.d.ts +0 -19
- package/dist/types/utils/oauth/kagi.d.ts +0 -17
- package/dist/types/utils/oauth/kilo.d.ts +0 -5
- package/dist/types/utils/oauth/litellm.d.ts +0 -18
- package/dist/types/utils/oauth/lm-studio.d.ts +0 -17
- package/dist/types/utils/oauth/moonshot.d.ts +0 -1
- package/dist/types/utils/oauth/nanogpt.d.ts +0 -1
- package/dist/types/utils/oauth/nvidia.d.ts +0 -18
- package/dist/types/utils/oauth/ollama-cloud.d.ts +0 -2
- package/dist/types/utils/oauth/ollama.d.ts +0 -18
- package/dist/types/utils/oauth/openrouter.d.ts +0 -1
- package/dist/types/utils/oauth/parallel.d.ts +0 -17
- package/dist/types/utils/oauth/qianfan.d.ts +0 -17
- package/dist/types/utils/oauth/qwen-portal.d.ts +0 -19
- package/dist/types/utils/oauth/synthetic.d.ts +0 -1
- package/dist/types/utils/oauth/tavily.d.ts +0 -17
- package/dist/types/utils/oauth/together.d.ts +0 -1
- package/dist/types/utils/oauth/venice.d.ts +0 -18
- package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +0 -18
- package/dist/types/utils/oauth/vllm.d.ts +0 -16
- package/dist/types/utils/oauth/zai.d.ts +0 -18
- package/dist/types/utils/oauth/zenmux.d.ts +0 -1
- package/dist/types/utils/oauth/zhipu.d.ts +0 -18
- package/src/model-cache.ts +0 -129
- package/src/model-manager.ts +0 -469
- package/src/model-thinking.ts +0 -756
- package/src/models.json +0 -60287
- package/src/models.json.d.ts +0 -9
- package/src/models.ts +0 -56
- package/src/provider-models/bundled-references.ts +0 -38
- package/src/provider-models/descriptors.ts +0 -364
- package/src/provider-models/google.ts +0 -88
- package/src/provider-models/index.ts +0 -5
- package/src/provider-models/ollama.ts +0 -153
- package/src/provider-models/openai-compat.ts +0 -2904
- package/src/provider-models/special.ts +0 -67
- package/src/utils/discovery/antigravity.ts +0 -261
- package/src/utils/discovery/codex.ts +0 -371
- package/src/utils/discovery/cursor.ts +0 -306
- package/src/utils/discovery/gemini.ts +0 -248
- package/src/utils/discovery/index.ts +0 -4
- package/src/utils/discovery/openai-compatible.ts +0 -224
- package/src/utils/oauth/gitlab-duo.ts +0 -123
- package/src/utils/oauth/index.ts +0 -502
- /package/dist/types/{utils/oauth/__tests__/xai-oauth.test.d.ts → providers/__tests__/google-auth.test.d.ts} +0 -0
- /package/dist/types/{utils → registry}/oauth/callback-server.d.ts +0 -0
- /package/dist/types/{utils → registry}/oauth/cursor.d.ts +0 -0
- /package/dist/types/{utils → registry}/oauth/gitlab-duo.d.ts +0 -0
- /package/dist/types/{utils → registry}/oauth/google-antigravity.d.ts +0 -0
- /package/dist/types/{utils → registry}/oauth/google-gemini-cli.d.ts +0 -0
- /package/dist/types/{utils → registry}/oauth/google-oauth-shared.d.ts +0 -0
- /package/dist/types/{utils → registry}/oauth/kimi.d.ts +0 -0
- /package/dist/types/{utils → registry}/oauth/openai-codex.d.ts +0 -0
- /package/dist/types/{utils → registry}/oauth/opencode.d.ts +0 -0
- /package/dist/types/{utils → registry}/oauth/perplexity.d.ts +0 -0
- /package/dist/types/{utils → registry}/oauth/pkce.d.ts +0 -0
- /package/dist/types/{utils → registry}/oauth/wafer.d.ts +0 -0
- /package/dist/types/{utils → registry}/oauth/xiaomi.d.ts +0 -0
- /package/src/{utils → registry}/oauth/callback-server.ts +0 -0
- /package/src/{utils → registry}/oauth/cursor.ts +0 -0
- /package/src/{utils → registry}/oauth/google-oauth-shared.ts +0 -0
- /package/src/{utils → registry}/oauth/kimi.ts +0 -0
- /package/src/{utils → registry}/oauth/oauth.html +0 -0
- /package/src/{utils → registry}/oauth/openai-codex.ts +0 -0
- /package/src/{utils → registry}/oauth/opencode.ts +0 -0
- /package/src/{utils → registry}/oauth/perplexity.ts +0 -0
- /package/src/{utils → registry}/oauth/pkce.ts +0 -0
package/src/auth-storage.ts
CHANGED
|
@@ -11,30 +11,42 @@ import { Database, type Statement } from "bun:sqlite";
|
|
|
11
11
|
import * as fs from "node:fs/promises";
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { getAgentDbPath, logger } from "@prometheus-ai/utils";
|
|
14
|
-
import {
|
|
14
|
+
import type { ApiKeyResolver } from "./auth-retry";
|
|
15
|
+
import { isUsageLimitError } from "./rate-limit-utils";
|
|
16
|
+
import { getProviderDefinition } from "./registry";
|
|
17
|
+
import { getOAuthApiKey, getOAuthProvider, refreshOAuthToken } from "./registry/oauth";
|
|
18
|
+
import type { OAuthController, OAuthCredentials, OAuthProvider, OAuthProviderId } from "./registry/oauth/types";
|
|
19
|
+
import { getEnvApiKey, getEnvApiKeyName } from "./stream";
|
|
15
20
|
import type { Provider } from "./types";
|
|
16
21
|
import type {
|
|
22
|
+
CredentialRankingContext,
|
|
17
23
|
CredentialRankingStrategy,
|
|
18
24
|
UsageCredential,
|
|
19
25
|
UsageFetchContext,
|
|
20
26
|
UsageFetchParams,
|
|
27
|
+
UsageHistoryEntry,
|
|
28
|
+
UsageHistoryQuery,
|
|
21
29
|
UsageLimit,
|
|
22
30
|
UsageLogger,
|
|
23
31
|
UsageProvider,
|
|
24
32
|
UsageReport,
|
|
25
33
|
} from "./usage";
|
|
34
|
+
import { resolveUsedFraction } from "./usage";
|
|
26
35
|
import { claudeRankingStrategy, claudeUsageProvider } from "./usage/claude";
|
|
27
36
|
import { googleGeminiCliUsageProvider } from "./usage/gemini";
|
|
28
37
|
import { githubCopilotUsageProvider } from "./usage/github-copilot";
|
|
29
|
-
import { antigravityUsageProvider } from "./usage/google-antigravity";
|
|
38
|
+
import { antigravityRankingStrategy, antigravityUsageProvider } from "./usage/google-antigravity";
|
|
30
39
|
import { kimiUsageProvider } from "./usage/kimi";
|
|
31
40
|
import { codexRankingStrategy, openaiCodexUsageProvider } from "./usage/openai-codex";
|
|
41
|
+
import {
|
|
42
|
+
type CodexResetConsumeCode,
|
|
43
|
+
type CodexResetCredit,
|
|
44
|
+
consumeCodexResetCredit,
|
|
45
|
+
listCodexResetCredits,
|
|
46
|
+
} from "./usage/openai-codex-reset";
|
|
32
47
|
import { zaiUsageProvider } from "./usage/zai";
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
import { loginOpenAICodexDevice } from "./utils/oauth/openai-codex";
|
|
36
|
-
import type { OAuthController, OAuthCredentials, OAuthProvider, OAuthProviderId } from "./utils/oauth/types";
|
|
37
|
-
import { loginXiaomi, loginXiaomiTokenPlan } from "./utils/oauth/xiaomi";
|
|
48
|
+
|
|
49
|
+
const USAGE_RANKING_METRIC_EPSILON = 1e-9;
|
|
38
50
|
|
|
39
51
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
40
52
|
// Credential Types
|
|
@@ -55,6 +67,23 @@ export type AuthCredentialEntry = AuthCredential | AuthCredential[];
|
|
|
55
67
|
|
|
56
68
|
export type AuthStorageData = Record<string, AuthCredentialEntry>;
|
|
57
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Cascade leg that supplies a provider's active credential, highest precedence
|
|
72
|
+
* first — mirrors {@link AuthStorage.getApiKey}'s resolution order.
|
|
73
|
+
*/
|
|
74
|
+
export type CredentialOriginKind = "runtime" | "config" | "oauth" | "api_key" | "env" | "fallback";
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Structured provenance for a provider's auth, for UI that needs a machine
|
|
78
|
+
* tag (the `/login` provider list) rather than the prose of
|
|
79
|
+
* {@link AuthStorage.describeCredentialSource}.
|
|
80
|
+
*/
|
|
81
|
+
export interface CredentialOrigin {
|
|
82
|
+
kind: CredentialOriginKind;
|
|
83
|
+
/** Env var name when `kind === "env"` and a single named variable backs it. */
|
|
84
|
+
envVar?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
58
87
|
/**
|
|
59
88
|
* Serialized representation of AuthStorage for passing to subagent workers.
|
|
60
89
|
* Contains only the essential credential data, not runtime state.
|
|
@@ -264,6 +293,14 @@ export interface AuthCredentialStore {
|
|
|
264
293
|
getCache(key: string, options?: { includeExpired?: boolean }): string | null;
|
|
265
294
|
setCache(key: string, value: string, expiresAtSec: number): void;
|
|
266
295
|
cleanExpiredCache(): void;
|
|
296
|
+
/**
|
|
297
|
+
* Append usage-limit snapshots for trend history. Optional: stores without
|
|
298
|
+
* durable storage (e.g. the broker remote store) omit it and recording is
|
|
299
|
+
* skipped — the broker host records into its own database instead.
|
|
300
|
+
*/
|
|
301
|
+
recordUsageSnapshots?(entries: UsageHistoryEntry[]): void;
|
|
302
|
+
/** Read recorded usage-limit snapshots, oldest first. */
|
|
303
|
+
listUsageHistory?(query?: UsageHistoryQuery): UsageHistoryEntry[];
|
|
267
304
|
/**
|
|
268
305
|
* Optional store-supplied OAuth refresh. When present, `AuthStorage` uses
|
|
269
306
|
* it before the per-provider local refresh path. `RemoteAuthCredentialStore`
|
|
@@ -339,6 +376,11 @@ export interface AuthCredentialStore {
|
|
|
339
376
|
* `replaceAuthCredentialsForProvider`.
|
|
340
377
|
*/
|
|
341
378
|
replaceAuthCredentialsRemote?(provider: string, credentials: AuthCredential[]): Promise<StoredAuthCredential[]>;
|
|
379
|
+
/**
|
|
380
|
+
* Optional async write hook for disabling one stored credential. Remote stores
|
|
381
|
+
* use it to await broker persistence before AuthStorage updates its snapshot.
|
|
382
|
+
*/
|
|
383
|
+
deleteAuthCredentialRemote?(id: number, disabledCause: string): Promise<boolean>;
|
|
342
384
|
/**
|
|
343
385
|
* Optional async write hook for clearing every credential for a provider
|
|
344
386
|
* (logout). When present, `AuthStorage.remove` routes through this instead
|
|
@@ -408,7 +450,7 @@ export type AuthStorageOptions = {
|
|
|
408
450
|
*
|
|
409
451
|
* Examples:
|
|
410
452
|
* - `"local ~/.prometheus/agent/agent.db"`
|
|
411
|
-
* - `"broker http://
|
|
453
|
+
* - `"broker http://auth-broker.internal:8765"`
|
|
412
454
|
*/
|
|
413
455
|
sourceLabel?: string;
|
|
414
456
|
/**
|
|
@@ -416,7 +458,7 @@ export type AuthStorageOptions = {
|
|
|
416
458
|
* calls this instead of fanning out per-credential. The primary use case is
|
|
417
459
|
* routing through a broker that egresses from a less-throttled IP — e.g. a
|
|
418
460
|
* residential laptop trips Anthropic's per-IP rate limit on the usage
|
|
419
|
-
* endpoint and drops 2-of-5 credentials, while the
|
|
461
|
+
* endpoint and drops 2-of-5 credentials, while the broker gets all 5.
|
|
420
462
|
*
|
|
421
463
|
* Implementations may return null when no usage data is available; the
|
|
422
464
|
* AuthStorage caller surfaces that to its own consumer unchanged.
|
|
@@ -463,6 +505,13 @@ const USAGE_CACHE_PREFIX = "usage_cache:";
|
|
|
463
505
|
const USAGE_REPORT_TTL_MS = 5 * 60_000;
|
|
464
506
|
const USAGE_HEADER_INGEST_INTERVAL_MS = 60_000;
|
|
465
507
|
const USAGE_LAST_GOOD_RETENTION_MS = 24 * 60 * 60_000;
|
|
508
|
+
/**
|
|
509
|
+
* Downsample usage history to at most one row per hour per account window: a
|
|
510
|
+
* snapshot landing in the same hour bucket as the series' latest row
|
|
511
|
+
* overwrites it in place. That bound makes further retention pruning
|
|
512
|
+
* unnecessary — 1 row/hour is ~9k rows per account window per year.
|
|
513
|
+
*/
|
|
514
|
+
const USAGE_HISTORY_BUCKET_MS = 60 * 60_000;
|
|
466
515
|
/**
|
|
467
516
|
* Per-credential cool-down after a usage fetch fails. While this window is
|
|
468
517
|
* active we serve the last successful value to avoid dropping the credential
|
|
@@ -473,6 +522,9 @@ const USAGE_FAILURE_BACKOFF_MS = 10_000;
|
|
|
473
522
|
// Bumped from 3s — Claude usage retries up to 3 times with exponential backoff
|
|
474
523
|
// (~3.5s total worst case); a tight per-request budget aborts retries mid-cycle.
|
|
475
524
|
const DEFAULT_USAGE_REQUEST_TIMEOUT_MS = 10_000;
|
|
525
|
+
const USAGE_REPORT_CACHE_KEY_VERSION_OVERRIDES: Partial<Record<Provider, number>> = {
|
|
526
|
+
"google-antigravity": 2,
|
|
527
|
+
};
|
|
476
528
|
const DEFAULT_OAUTH_REFRESH_TIMEOUT_MS = 10_000;
|
|
477
529
|
/**
|
|
478
530
|
* Refresh OAuth access tokens this many ms before their stated expiry. The
|
|
@@ -517,6 +569,23 @@ export function isDefinitiveOAuthFailure(errorMsg: string): boolean {
|
|
|
517
569
|
return false;
|
|
518
570
|
}
|
|
519
571
|
|
|
572
|
+
/**
|
|
573
|
+
* Outcome of {@link AuthStorage.markUsageLimitReached}.
|
|
574
|
+
*
|
|
575
|
+
* `switched` is `true` when an unblocked same-type sibling credential is
|
|
576
|
+
* available right now, so the caller can retry immediately and the next
|
|
577
|
+
* `getApiKey` will hand it out. When `false`, `retryAtMs` (epoch ms) carries
|
|
578
|
+
* the earliest moment any same-type sibling's temporary block expires —
|
|
579
|
+
* callers should prefer waiting until then over the provider's (often
|
|
580
|
+
* multi-hour) retry-after when it is sooner. `retryAtMs` is `undefined` when
|
|
581
|
+
* no sibling credentials exist at all, or when the session has no tracked
|
|
582
|
+
* credential to rotate away from.
|
|
583
|
+
*/
|
|
584
|
+
export interface UsageLimitMarkResult {
|
|
585
|
+
switched: boolean;
|
|
586
|
+
retryAtMs?: number;
|
|
587
|
+
}
|
|
588
|
+
|
|
520
589
|
type UsageCacheEntry<T> = {
|
|
521
590
|
value: T;
|
|
522
591
|
expiresAt: number;
|
|
@@ -544,6 +613,13 @@ type AuthApiKeyOptions = {
|
|
|
544
613
|
* stranding the caller for `timeoutMs * (maxRetries + 1)`.
|
|
545
614
|
*/
|
|
546
615
|
signal?: AbortSignal;
|
|
616
|
+
/**
|
|
617
|
+
* Force a re-mint of the session-preferred OAuth credential's access token,
|
|
618
|
+
* bypassing the not-yet-expired short-circuit. Powers step (b) of the
|
|
619
|
+
* auth-retry policy ("refresh the SAME account") so a locally-cached token
|
|
620
|
+
* that a peer/broker rotated out from under us is replaced before retrying.
|
|
621
|
+
*/
|
|
622
|
+
forceRefresh?: boolean;
|
|
547
623
|
};
|
|
548
624
|
type OAuthResolutionResult = { apiKey: string; credential: OAuthCredential };
|
|
549
625
|
|
|
@@ -573,12 +649,65 @@ export interface OAuthAccessFailure {
|
|
|
573
649
|
error: string;
|
|
574
650
|
}
|
|
575
651
|
|
|
652
|
+
/**
|
|
653
|
+
* Identity of the OAuth credential a session is currently routed to. Read-only
|
|
654
|
+
* display/metadata shape: `accountId` is the provider's account UUID, `email`
|
|
655
|
+
* the user-facing login, `projectId` the GCP-style project for providers that
|
|
656
|
+
* key usage on it (Gemini CLI / Antigravity).
|
|
657
|
+
*/
|
|
658
|
+
export interface OAuthAccountIdentity {
|
|
659
|
+
accountId?: string;
|
|
660
|
+
email?: string;
|
|
661
|
+
projectId?: string;
|
|
662
|
+
}
|
|
663
|
+
|
|
576
664
|
export type OAuthAccessResolution = ({ ok: true } & OAuthAccess) | ({ ok: false } & OAuthAccessFailure);
|
|
577
665
|
export interface InvalidateCredentialMatchingOptions {
|
|
578
666
|
signal?: AbortSignal;
|
|
579
667
|
sessionId?: string;
|
|
580
668
|
}
|
|
581
669
|
|
|
670
|
+
/**
|
|
671
|
+
* Identifies which stored account to redeem a saved rate-limit reset for.
|
|
672
|
+
* Any one field is enough; `credentialId` is the most precise.
|
|
673
|
+
*/
|
|
674
|
+
export interface ResetCreditTarget {
|
|
675
|
+
credentialId?: number;
|
|
676
|
+
accountId?: string;
|
|
677
|
+
email?: string;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/** Outcome of {@link AuthStorage.redeemResetCredit}. */
|
|
681
|
+
export interface ResetCreditRedeemOutcome {
|
|
682
|
+
/** `true` only when a reset was actually applied (`code === "reset"`). */
|
|
683
|
+
ok: boolean;
|
|
684
|
+
/**
|
|
685
|
+
* Result code. Backend codes: `reset` (success), `already_redeemed`,
|
|
686
|
+
* `no_credit`, `nothing_to_reset`. Locally-synthesized: `no_account`
|
|
687
|
+
* (target not found), `account_unavailable` (token refresh failed),
|
|
688
|
+
* `http_<status>` (unexpected HTTP).
|
|
689
|
+
*/
|
|
690
|
+
code: CodexResetConsumeCode;
|
|
691
|
+
accountId?: string;
|
|
692
|
+
email?: string;
|
|
693
|
+
/** The credit that was spent (when one was). */
|
|
694
|
+
creditId?: string;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** One stored account's live saved-reset status, from {@link AuthStorage.listResetCredits}. */
|
|
698
|
+
export interface ResetCreditAccountStatus {
|
|
699
|
+
credentialId?: number;
|
|
700
|
+
accountId?: string;
|
|
701
|
+
email?: string;
|
|
702
|
+
/** Resets redeemable for this account right now (live, not cached). */
|
|
703
|
+
availableCount: number;
|
|
704
|
+
credits: CodexResetCredit[];
|
|
705
|
+
/** Whether this is the given session's active account. */
|
|
706
|
+
active: boolean;
|
|
707
|
+
/** Set when the account's token refresh or list call failed. */
|
|
708
|
+
error?: string;
|
|
709
|
+
}
|
|
710
|
+
|
|
582
711
|
function isAbortSignalOption(
|
|
583
712
|
value: InvalidateCredentialMatchingOptions | AbortSignal | undefined,
|
|
584
713
|
): value is AbortSignal {
|
|
@@ -606,6 +735,14 @@ function hasOpenAICodexProPlan(report: UsageReport | null): boolean {
|
|
|
606
735
|
return getUsagePlanType(report)?.includes("pro") === true;
|
|
607
736
|
}
|
|
608
737
|
|
|
738
|
+
function compareUsageRankingMetric(left: number, right: number): number {
|
|
739
|
+
if (left === right) return 0;
|
|
740
|
+
if (!Number.isFinite(left) || !Number.isFinite(right)) return left < right ? -1 : 1;
|
|
741
|
+
const delta = left - right;
|
|
742
|
+
const tolerance = Math.max(USAGE_RANKING_METRIC_EPSILON, Math.max(Math.abs(left), Math.abs(right)) * 0.000001);
|
|
743
|
+
return Math.abs(delta) <= tolerance ? 0 : delta;
|
|
744
|
+
}
|
|
745
|
+
|
|
609
746
|
function resolveDefaultUsageProvider(provider: Provider): UsageProvider | undefined {
|
|
610
747
|
return DEFAULT_USAGE_PROVIDER_MAP.get(provider);
|
|
611
748
|
}
|
|
@@ -613,6 +750,7 @@ function resolveDefaultUsageProvider(provider: Provider): UsageProvider | undefi
|
|
|
613
750
|
const DEFAULT_RANKING_STRATEGIES = new Map<Provider, CredentialRankingStrategy>([
|
|
614
751
|
["openai-codex", codexRankingStrategy],
|
|
615
752
|
["anthropic", claudeRankingStrategy],
|
|
753
|
+
["google-antigravity", antigravityRankingStrategy],
|
|
616
754
|
]);
|
|
617
755
|
|
|
618
756
|
function resolveDefaultRankingStrategy(provider: Provider): CredentialRankingStrategy | undefined {
|
|
@@ -816,6 +954,14 @@ export class AuthStorage {
|
|
|
816
954
|
this.#usageProviderResolver = options.usageProviderResolver ?? resolveDefaultUsageProvider;
|
|
817
955
|
this.#rankingStrategyResolver = options.rankingStrategyResolver ?? resolveDefaultRankingStrategy;
|
|
818
956
|
this.#usageCache = new AuthStorageUsageCache(this.#store);
|
|
957
|
+
// Opportunistic hygiene, once per AuthStorage lifetime: drop expired
|
|
958
|
+
// cache rows (24h last-good retention). A cheap indexed DELETE;
|
|
959
|
+
// failures must never block construction.
|
|
960
|
+
try {
|
|
961
|
+
this.#store.cleanExpiredCache();
|
|
962
|
+
} catch {
|
|
963
|
+
// Best-effort.
|
|
964
|
+
}
|
|
819
965
|
this.#usageFetch = options.usageFetch ?? fetch;
|
|
820
966
|
this.#usageRequestTimeoutMs = options.usageRequestTimeoutMs ?? DEFAULT_USAGE_REQUEST_TIMEOUT_MS;
|
|
821
967
|
this.#refreshOAuthCredentialOverride = options.refreshOAuthCredential;
|
|
@@ -836,7 +982,7 @@ export class AuthStorage {
|
|
|
836
982
|
|
|
837
983
|
/**
|
|
838
984
|
* Create an AuthStorage instance backed by a AuthCredentialStore.
|
|
839
|
-
* Convenience factory for standalone use (e.g.,
|
|
985
|
+
* Convenience factory for standalone use (e.g., Prometheus AI CLI).
|
|
840
986
|
* @param dbPath - Path to SQLite database
|
|
841
987
|
*/
|
|
842
988
|
static async create(dbPath: string, options: AuthStorageOptions = {}): Promise<AuthStorage> {
|
|
@@ -1130,33 +1276,58 @@ export class AuthStorage {
|
|
|
1130
1276
|
return order;
|
|
1131
1277
|
}
|
|
1132
1278
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1279
|
+
#toScopedBackoffKey(providerKey: string, blockScope: string | undefined): string {
|
|
1280
|
+
return blockScope ? `${providerKey}\0${blockScope}` : providerKey;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/** Returns block expiry timestamp for a credential/key pair, cleaning up expired entries. */
|
|
1284
|
+
#getCredentialBlockedUntilForKey(backoffKey: string, credentialIndex: number): number | undefined {
|
|
1285
|
+
const backoffMap = this.#credentialBackoff.get(backoffKey);
|
|
1136
1286
|
if (!backoffMap) return undefined;
|
|
1137
1287
|
const blockedUntil = backoffMap.get(credentialIndex);
|
|
1138
1288
|
if (!blockedUntil) return undefined;
|
|
1139
1289
|
if (blockedUntil <= Date.now()) {
|
|
1140
1290
|
backoffMap.delete(credentialIndex);
|
|
1141
1291
|
if (backoffMap.size === 0) {
|
|
1142
|
-
this.#credentialBackoff.delete(
|
|
1292
|
+
this.#credentialBackoff.delete(backoffKey);
|
|
1143
1293
|
}
|
|
1144
1294
|
return undefined;
|
|
1145
1295
|
}
|
|
1146
1296
|
return blockedUntil;
|
|
1147
1297
|
}
|
|
1148
1298
|
|
|
1299
|
+
/** Returns block expiry timestamp for a credential, checking global then scoped blocks. */
|
|
1300
|
+
#getCredentialBlockedUntil(
|
|
1301
|
+
providerKey: string,
|
|
1302
|
+
credentialIndex: number,
|
|
1303
|
+
blockScope: string | undefined = undefined,
|
|
1304
|
+
): number | undefined {
|
|
1305
|
+
const globalBlockedUntil = this.#getCredentialBlockedUntilForKey(providerKey, credentialIndex);
|
|
1306
|
+
if (globalBlockedUntil !== undefined || !blockScope) return globalBlockedUntil;
|
|
1307
|
+
return this.#getCredentialBlockedUntilForKey(this.#toScopedBackoffKey(providerKey, blockScope), credentialIndex);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1149
1310
|
/** Checks if a credential is temporarily blocked due to usage limits. */
|
|
1150
|
-
#isCredentialBlocked(
|
|
1151
|
-
|
|
1311
|
+
#isCredentialBlocked(
|
|
1312
|
+
providerKey: string,
|
|
1313
|
+
credentialIndex: number,
|
|
1314
|
+
blockScope: string | undefined = undefined,
|
|
1315
|
+
): boolean {
|
|
1316
|
+
return this.#getCredentialBlockedUntil(providerKey, credentialIndex, blockScope) !== undefined;
|
|
1152
1317
|
}
|
|
1153
1318
|
|
|
1154
1319
|
/** Marks a credential as blocked until the specified time. */
|
|
1155
|
-
#markCredentialBlocked(
|
|
1156
|
-
|
|
1320
|
+
#markCredentialBlocked(
|
|
1321
|
+
providerKey: string,
|
|
1322
|
+
credentialIndex: number,
|
|
1323
|
+
blockedUntilMs: number,
|
|
1324
|
+
blockScope: string | undefined = undefined,
|
|
1325
|
+
): void {
|
|
1326
|
+
const backoffKey = this.#toScopedBackoffKey(providerKey, blockScope);
|
|
1327
|
+
const backoffMap = this.#credentialBackoff.get(backoffKey) ?? new Map<number, number>();
|
|
1157
1328
|
const existing = backoffMap.get(credentialIndex) ?? 0;
|
|
1158
1329
|
backoffMap.set(credentialIndex, Math.max(existing, blockedUntilMs));
|
|
1159
|
-
this.#credentialBackoff.set(
|
|
1330
|
+
this.#credentialBackoff.set(backoffKey, backoffMap);
|
|
1160
1331
|
}
|
|
1161
1332
|
|
|
1162
1333
|
/** Records which credential was used for a session (for rate-limit switching). */
|
|
@@ -1352,6 +1523,32 @@ export class AuthStorage {
|
|
|
1352
1523
|
this.#resetProviderAssignments(provider);
|
|
1353
1524
|
}
|
|
1354
1525
|
|
|
1526
|
+
/**
|
|
1527
|
+
* List stored credential rows, optionally filtered by provider.
|
|
1528
|
+
*/
|
|
1529
|
+
listStoredCredentials(provider?: string): StoredAuthCredential[] {
|
|
1530
|
+
if (provider !== undefined) {
|
|
1531
|
+
return this.#getStoredCredentials(provider).map(entry => ({
|
|
1532
|
+
id: entry.id,
|
|
1533
|
+
provider,
|
|
1534
|
+
credential: entry.credential,
|
|
1535
|
+
disabledCause: null,
|
|
1536
|
+
}));
|
|
1537
|
+
}
|
|
1538
|
+
const rows: StoredAuthCredential[] = [];
|
|
1539
|
+
for (const [storedProvider, entries] of this.#data) {
|
|
1540
|
+
for (const entry of entries) {
|
|
1541
|
+
rows.push({
|
|
1542
|
+
id: entry.id,
|
|
1543
|
+
provider: storedProvider,
|
|
1544
|
+
credential: entry.credential,
|
|
1545
|
+
disabledCause: null,
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
return rows;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1355
1552
|
/**
|
|
1356
1553
|
* Remove credential for a provider.
|
|
1357
1554
|
*/
|
|
@@ -1365,6 +1562,28 @@ export class AuthStorage {
|
|
|
1365
1562
|
this.#resetProviderAssignments(provider);
|
|
1366
1563
|
}
|
|
1367
1564
|
|
|
1565
|
+
/**
|
|
1566
|
+
* Remove one stored credential for a provider.
|
|
1567
|
+
*/
|
|
1568
|
+
async removeCredential(provider: string, credentialId: number): Promise<boolean> {
|
|
1569
|
+
const entries = this.#getStoredCredentials(provider);
|
|
1570
|
+
const index = entries.findIndex(entry => entry.id === credentialId);
|
|
1571
|
+
if (index === -1) return false;
|
|
1572
|
+
|
|
1573
|
+
if (this.#store.deleteAuthCredentialRemote) {
|
|
1574
|
+
const deleted = await this.#store.deleteAuthCredentialRemote(credentialId, "deleted by user");
|
|
1575
|
+
if (!deleted) return false;
|
|
1576
|
+
} else {
|
|
1577
|
+
this.#store.deleteAuthCredential(credentialId, "deleted by user");
|
|
1578
|
+
}
|
|
1579
|
+
this.#setStoredCredentials(
|
|
1580
|
+
provider,
|
|
1581
|
+
entries.filter((_entry, entryIndex) => entryIndex !== index),
|
|
1582
|
+
);
|
|
1583
|
+
this.#resetProviderAssignments(provider);
|
|
1584
|
+
return true;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1368
1587
|
/**
|
|
1369
1588
|
* List all providers with credentials.
|
|
1370
1589
|
*/
|
|
@@ -1411,6 +1630,26 @@ export class AuthStorage {
|
|
|
1411
1630
|
return false;
|
|
1412
1631
|
}
|
|
1413
1632
|
|
|
1633
|
+
/**
|
|
1634
|
+
* Classify where a provider's auth comes from, following the same precedence
|
|
1635
|
+
* as {@link AuthStorage.getApiKey}: runtime override → config override →
|
|
1636
|
+
* stored credential (api_key before oauth, matching getApiKey) → env var →
|
|
1637
|
+
* fallback resolver. Returns undefined when no auth is configured.
|
|
1638
|
+
*
|
|
1639
|
+
* Compact, structured counterpart to {@link describeCredentialSource}.
|
|
1640
|
+
*/
|
|
1641
|
+
getCredentialOrigin(provider: string): CredentialOrigin | undefined {
|
|
1642
|
+
if (this.#runtimeOverrides.has(provider)) return { kind: "runtime" };
|
|
1643
|
+
if (this.#configOverrides.has(provider)) return { kind: "config" };
|
|
1644
|
+
const stored = this.#getCredentialsForProvider(provider);
|
|
1645
|
+
if (stored.length > 0) {
|
|
1646
|
+
return { kind: stored.some(credential => credential.type === "api_key") ? "api_key" : "oauth" };
|
|
1647
|
+
}
|
|
1648
|
+
if (getEnvApiKey(provider)) return { kind: "env", envVar: getEnvApiKeyName(provider) };
|
|
1649
|
+
if (this.#fallbackResolver?.(provider)) return { kind: "fallback" };
|
|
1650
|
+
return undefined;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1414
1653
|
/**
|
|
1415
1654
|
* Check if OAuth credentials are configured for a provider.
|
|
1416
1655
|
*/
|
|
@@ -1470,6 +1709,28 @@ export class AuthStorage {
|
|
|
1470
1709
|
return typeof accountId === "string" && accountId.length > 0 ? accountId : undefined;
|
|
1471
1710
|
}
|
|
1472
1711
|
|
|
1712
|
+
/**
|
|
1713
|
+
* Get the OAuth account identity for a provider, preferring the credential that
|
|
1714
|
+
* is session-sticky for `sessionId`. This is a read-only lookup for display and
|
|
1715
|
+
* metadata paths; it does not refresh tokens, rank usage, or advance selection.
|
|
1716
|
+
*/
|
|
1717
|
+
getOAuthAccountIdentity(provider: string, sessionId?: string): OAuthAccountIdentity | undefined {
|
|
1718
|
+
const preferred = this.#resolveActiveOAuthCredential(provider, sessionId);
|
|
1719
|
+
if (!preferred) return undefined;
|
|
1720
|
+
const identity: OAuthAccountIdentity = {};
|
|
1721
|
+
if (typeof preferred.accountId === "string" && preferred.accountId.length > 0) {
|
|
1722
|
+
identity.accountId = preferred.accountId;
|
|
1723
|
+
}
|
|
1724
|
+
if (typeof preferred.email === "string" && preferred.email.length > 0) {
|
|
1725
|
+
identity.email = preferred.email;
|
|
1726
|
+
}
|
|
1727
|
+
if (typeof preferred.projectId === "string" && preferred.projectId.length > 0) {
|
|
1728
|
+
identity.projectId = preferred.projectId;
|
|
1729
|
+
}
|
|
1730
|
+
if (!identity.accountId && !identity.email && !identity.projectId) return undefined;
|
|
1731
|
+
return identity;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1473
1734
|
/**
|
|
1474
1735
|
* Get all credentials.
|
|
1475
1736
|
*/
|
|
@@ -1498,345 +1759,34 @@ export class AuthStorage {
|
|
|
1498
1759
|
onPrompt: (prompt: { message: string; placeholder?: string }) => Promise<string>;
|
|
1499
1760
|
},
|
|
1500
1761
|
): Promise<void> {
|
|
1501
|
-
let credentials: OAuthCredentials;
|
|
1502
1762
|
const saveApiKeyCredential = async (apiKey: string): Promise<void> => {
|
|
1503
1763
|
const newCredential: ApiKeyCredential = { type: "api_key", key: apiKey };
|
|
1504
1764
|
await this.set(provider, newCredential);
|
|
1505
1765
|
};
|
|
1506
1766
|
const manualCodeInput = () => ctrl.onPrompt({ message: "Paste the authorization code (or full redirect URL):" });
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
}
|
|
1524
|
-
case "alibaba-coding-plan": {
|
|
1525
|
-
const { loginAlibabaCodingPlan } = await import("./utils/oauth/alibaba-coding-plan");
|
|
1526
|
-
const apiKey = await loginAlibabaCodingPlan(ctrl);
|
|
1527
|
-
await saveApiKeyCredential(apiKey);
|
|
1528
|
-
return;
|
|
1529
|
-
}
|
|
1530
|
-
case "github-copilot": {
|
|
1531
|
-
const { loginGitHubCopilot } = await import("./utils/oauth/github-copilot");
|
|
1532
|
-
credentials = await loginGitHubCopilot({
|
|
1533
|
-
onAuth: (url, instructions) => ctrl.onAuth({ url, instructions }),
|
|
1534
|
-
onPrompt: ctrl.onPrompt,
|
|
1535
|
-
onProgress: ctrl.onProgress,
|
|
1536
|
-
signal: ctrl.signal,
|
|
1537
|
-
});
|
|
1538
|
-
break;
|
|
1539
|
-
}
|
|
1540
|
-
case "google-gemini-cli": {
|
|
1541
|
-
const { loginGeminiCli } = await import("./utils/oauth/google-gemini-cli");
|
|
1542
|
-
credentials = await loginGeminiCli({
|
|
1543
|
-
...ctrl,
|
|
1544
|
-
onManualCodeInput: ctrl.onManualCodeInput ?? manualCodeInput,
|
|
1545
|
-
});
|
|
1546
|
-
break;
|
|
1547
|
-
}
|
|
1548
|
-
case "google-antigravity": {
|
|
1549
|
-
const { loginAntigravity } = await import("./utils/oauth/google-antigravity");
|
|
1550
|
-
credentials = await loginAntigravity({
|
|
1551
|
-
...ctrl,
|
|
1552
|
-
onManualCodeInput: ctrl.onManualCodeInput ?? manualCodeInput,
|
|
1553
|
-
});
|
|
1554
|
-
break;
|
|
1555
|
-
}
|
|
1556
|
-
case "openai-codex": {
|
|
1557
|
-
const { loginOpenAICodex } = await import("./utils/oauth/openai-codex");
|
|
1558
|
-
credentials = await loginOpenAICodex({
|
|
1559
|
-
...ctrl,
|
|
1560
|
-
onManualCodeInput: ctrl.onManualCodeInput ?? manualCodeInput,
|
|
1561
|
-
});
|
|
1562
|
-
break;
|
|
1563
|
-
}
|
|
1564
|
-
case "openai-codex-device": {
|
|
1565
|
-
// Device/headless flow — stores credentials under "openai-codex" so the
|
|
1566
|
-
// provider can pick them up without a separate provider configuration.
|
|
1567
|
-
const deviceCredentials = await loginOpenAICodexDevice(ctrl);
|
|
1568
|
-
const newCredential: OAuthCredential = { type: "oauth", ...deviceCredentials };
|
|
1569
|
-
await this.#upsertOAuthCredential("openai-codex", newCredential);
|
|
1570
|
-
return;
|
|
1571
|
-
}
|
|
1572
|
-
case "gitlab-duo": {
|
|
1573
|
-
const { loginGitLabDuo } = await import("./utils/oauth/gitlab-duo");
|
|
1574
|
-
credentials = await loginGitLabDuo({
|
|
1575
|
-
...ctrl,
|
|
1576
|
-
onManualCodeInput: ctrl.onManualCodeInput ?? manualCodeInput,
|
|
1577
|
-
});
|
|
1578
|
-
break;
|
|
1579
|
-
}
|
|
1580
|
-
case "kimi-code": {
|
|
1581
|
-
const { loginKimi } = await import("./utils/oauth/kimi");
|
|
1582
|
-
credentials = await loginKimi(ctrl);
|
|
1583
|
-
break;
|
|
1584
|
-
}
|
|
1585
|
-
case "kilo": {
|
|
1586
|
-
const { loginKilo } = await import("./utils/oauth/kilo");
|
|
1587
|
-
credentials = await loginKilo(ctrl);
|
|
1588
|
-
break;
|
|
1589
|
-
}
|
|
1590
|
-
case "cursor": {
|
|
1591
|
-
const { loginCursor } = await import("./utils/oauth/cursor");
|
|
1592
|
-
credentials = await loginCursor(
|
|
1593
|
-
url => ctrl.onAuth({ url }),
|
|
1594
|
-
ctrl.onProgress ? () => ctrl.onProgress?.("Waiting for browser authentication...") : undefined,
|
|
1595
|
-
);
|
|
1596
|
-
break;
|
|
1597
|
-
}
|
|
1598
|
-
case "perplexity": {
|
|
1599
|
-
const { loginPerplexity } = await import("./utils/oauth/perplexity");
|
|
1600
|
-
credentials = await loginPerplexity(ctrl);
|
|
1601
|
-
break;
|
|
1602
|
-
}
|
|
1603
|
-
case "huggingface": {
|
|
1604
|
-
const { loginHuggingface } = await import("./utils/oauth/huggingface");
|
|
1605
|
-
const apiKey = await loginHuggingface(ctrl);
|
|
1606
|
-
await saveApiKeyCredential(apiKey);
|
|
1607
|
-
return;
|
|
1608
|
-
}
|
|
1609
|
-
case "opencode-zen":
|
|
1610
|
-
case "opencode-go": {
|
|
1611
|
-
const { loginOpenCode } = await import("./utils/oauth/opencode");
|
|
1612
|
-
const apiKey = await loginOpenCode(ctrl);
|
|
1613
|
-
await saveApiKeyCredential(apiKey);
|
|
1614
|
-
return;
|
|
1615
|
-
}
|
|
1616
|
-
case "lm-studio": {
|
|
1617
|
-
const { loginLmStudio } = await import("./utils/oauth/lm-studio");
|
|
1618
|
-
const apiKey = await loginLmStudio(ctrl);
|
|
1619
|
-
await saveApiKeyCredential(apiKey);
|
|
1620
|
-
return;
|
|
1621
|
-
}
|
|
1622
|
-
case "ollama": {
|
|
1623
|
-
const { loginOllama } = await import("./utils/oauth/ollama");
|
|
1624
|
-
const apiKey = await loginOllama(ctrl);
|
|
1625
|
-
if (!apiKey) {
|
|
1626
|
-
return;
|
|
1627
|
-
}
|
|
1628
|
-
await saveApiKeyCredential(apiKey);
|
|
1629
|
-
return;
|
|
1630
|
-
}
|
|
1631
|
-
case "ollama-cloud": {
|
|
1632
|
-
const { loginOllamaCloud } = await import("./utils/oauth/ollama-cloud");
|
|
1633
|
-
const apiKey = await loginOllamaCloud(ctrl);
|
|
1634
|
-
await saveApiKeyCredential(apiKey);
|
|
1635
|
-
return;
|
|
1636
|
-
}
|
|
1637
|
-
case "cerebras": {
|
|
1638
|
-
const { loginCerebras } = await import("./utils/oauth/cerebras");
|
|
1639
|
-
const apiKey = await loginCerebras(ctrl);
|
|
1640
|
-
await saveApiKeyCredential(apiKey);
|
|
1641
|
-
return;
|
|
1642
|
-
}
|
|
1643
|
-
case "deepseek": {
|
|
1644
|
-
const apiKey = await loginDeepSeek(ctrl);
|
|
1645
|
-
await saveApiKeyCredential(apiKey);
|
|
1646
|
-
return;
|
|
1647
|
-
}
|
|
1648
|
-
case "fireworks": {
|
|
1649
|
-
const { loginFireworks } = await import("./utils/oauth/fireworks");
|
|
1650
|
-
const apiKey = await loginFireworks(ctrl);
|
|
1651
|
-
await saveApiKeyCredential(apiKey);
|
|
1652
|
-
return;
|
|
1653
|
-
}
|
|
1654
|
-
case "firepass": {
|
|
1655
|
-
const { loginFirepass } = await import("./utils/oauth/firepass");
|
|
1656
|
-
const apiKey = await loginFirepass(ctrl);
|
|
1657
|
-
await saveApiKeyCredential(apiKey);
|
|
1658
|
-
return;
|
|
1659
|
-
}
|
|
1660
|
-
case "wafer-pass": {
|
|
1661
|
-
const { loginWaferPass } = await import("./utils/oauth/wafer");
|
|
1662
|
-
const apiKey = await loginWaferPass(ctrl);
|
|
1663
|
-
await saveApiKeyCredential(apiKey);
|
|
1664
|
-
return;
|
|
1665
|
-
}
|
|
1666
|
-
case "wafer-serverless": {
|
|
1667
|
-
const { loginWaferServerless } = await import("./utils/oauth/wafer");
|
|
1668
|
-
const apiKey = await loginWaferServerless(ctrl);
|
|
1669
|
-
await saveApiKeyCredential(apiKey);
|
|
1670
|
-
return;
|
|
1671
|
-
}
|
|
1672
|
-
case "zai": {
|
|
1673
|
-
const { loginZai } = await import("./utils/oauth/zai");
|
|
1674
|
-
const apiKey = await loginZai(ctrl);
|
|
1675
|
-
await saveApiKeyCredential(apiKey);
|
|
1676
|
-
return;
|
|
1677
|
-
}
|
|
1678
|
-
case "zhipu-coding-plan": {
|
|
1679
|
-
const { loginZhipuCodingPlan } = await import("./utils/oauth/zhipu");
|
|
1680
|
-
const apiKey = await loginZhipuCodingPlan(ctrl);
|
|
1681
|
-
await saveApiKeyCredential(apiKey);
|
|
1682
|
-
return;
|
|
1683
|
-
}
|
|
1684
|
-
case "qianfan": {
|
|
1685
|
-
const { loginQianfan } = await import("./utils/oauth/qianfan");
|
|
1686
|
-
const apiKey = await loginQianfan(ctrl);
|
|
1687
|
-
await saveApiKeyCredential(apiKey);
|
|
1688
|
-
return;
|
|
1689
|
-
}
|
|
1690
|
-
case "minimax-code": {
|
|
1691
|
-
const { loginMiniMaxCode } = await import("./utils/oauth/minimax-code");
|
|
1692
|
-
const apiKey = await loginMiniMaxCode(ctrl);
|
|
1693
|
-
await saveApiKeyCredential(apiKey);
|
|
1694
|
-
return;
|
|
1695
|
-
}
|
|
1696
|
-
case "minimax-code-cn": {
|
|
1697
|
-
const { loginMiniMaxCodeCn } = await import("./utils/oauth/minimax-code");
|
|
1698
|
-
const apiKey = await loginMiniMaxCodeCn(ctrl);
|
|
1699
|
-
await saveApiKeyCredential(apiKey);
|
|
1700
|
-
return;
|
|
1701
|
-
}
|
|
1702
|
-
case "synthetic": {
|
|
1703
|
-
const { loginSynthetic } = await import("./utils/oauth/synthetic");
|
|
1704
|
-
const apiKey = await loginSynthetic(ctrl);
|
|
1705
|
-
await saveApiKeyCredential(apiKey);
|
|
1706
|
-
return;
|
|
1707
|
-
}
|
|
1708
|
-
case "tavily": {
|
|
1709
|
-
const { loginTavily } = await import("./utils/oauth/tavily");
|
|
1710
|
-
const apiKey = await loginTavily(ctrl);
|
|
1711
|
-
await saveApiKeyCredential(apiKey);
|
|
1712
|
-
return;
|
|
1713
|
-
}
|
|
1714
|
-
case "venice": {
|
|
1715
|
-
const { loginVenice } = await import("./utils/oauth/venice");
|
|
1716
|
-
const apiKey = await loginVenice(ctrl);
|
|
1717
|
-
await saveApiKeyCredential(apiKey);
|
|
1718
|
-
return;
|
|
1719
|
-
}
|
|
1720
|
-
case "litellm": {
|
|
1721
|
-
const { loginLiteLLM } = await import("./utils/oauth/litellm");
|
|
1722
|
-
const apiKey = await loginLiteLLM(ctrl);
|
|
1723
|
-
await saveApiKeyCredential(apiKey);
|
|
1724
|
-
return;
|
|
1725
|
-
}
|
|
1726
|
-
case "moonshot": {
|
|
1727
|
-
const { loginMoonshot } = await import("./utils/oauth/moonshot");
|
|
1728
|
-
const apiKey = await loginMoonshot(ctrl);
|
|
1729
|
-
await saveApiKeyCredential(apiKey);
|
|
1730
|
-
return;
|
|
1731
|
-
}
|
|
1732
|
-
case "kagi": {
|
|
1733
|
-
const { loginKagi } = await import("./utils/oauth/kagi");
|
|
1734
|
-
const apiKey = await loginKagi(ctrl);
|
|
1735
|
-
await saveApiKeyCredential(apiKey);
|
|
1736
|
-
return;
|
|
1737
|
-
}
|
|
1738
|
-
case "nanogpt": {
|
|
1739
|
-
const { loginNanoGPT } = await import("./utils/oauth/nanogpt");
|
|
1740
|
-
const apiKey = await loginNanoGPT(ctrl);
|
|
1741
|
-
await saveApiKeyCredential(apiKey);
|
|
1742
|
-
return;
|
|
1743
|
-
}
|
|
1744
|
-
case "openrouter": {
|
|
1745
|
-
const { loginOpenRouter } = await import("./utils/oauth/openrouter");
|
|
1746
|
-
const apiKey = await loginOpenRouter(ctrl);
|
|
1747
|
-
await saveApiKeyCredential(apiKey);
|
|
1748
|
-
return;
|
|
1749
|
-
}
|
|
1750
|
-
case "together": {
|
|
1751
|
-
const { loginTogether } = await import("./utils/oauth/together");
|
|
1752
|
-
const apiKey = await loginTogether(ctrl);
|
|
1753
|
-
await saveApiKeyCredential(apiKey);
|
|
1754
|
-
return;
|
|
1755
|
-
}
|
|
1756
|
-
case "cloudflare-ai-gateway": {
|
|
1757
|
-
const { loginCloudflareAiGateway } = await import("./utils/oauth/cloudflare-ai-gateway");
|
|
1758
|
-
const apiKey = await loginCloudflareAiGateway(ctrl);
|
|
1759
|
-
await saveApiKeyCredential(apiKey);
|
|
1760
|
-
return;
|
|
1761
|
-
}
|
|
1762
|
-
case "vercel-ai-gateway": {
|
|
1763
|
-
const { loginVercelAiGateway } = await import("./utils/oauth/vercel-ai-gateway");
|
|
1764
|
-
const apiKey = await loginVercelAiGateway(ctrl);
|
|
1765
|
-
await saveApiKeyCredential(apiKey);
|
|
1766
|
-
return;
|
|
1767
|
-
}
|
|
1768
|
-
case "vllm": {
|
|
1769
|
-
const { loginVllm } = await import("./utils/oauth/vllm");
|
|
1770
|
-
const apiKey = await loginVllm(ctrl);
|
|
1771
|
-
await saveApiKeyCredential(apiKey);
|
|
1772
|
-
return;
|
|
1773
|
-
}
|
|
1774
|
-
case "parallel": {
|
|
1775
|
-
const { loginParallel } = await import("./utils/oauth/parallel");
|
|
1776
|
-
const apiKey = await loginParallel(ctrl);
|
|
1777
|
-
await saveApiKeyCredential(apiKey);
|
|
1778
|
-
return;
|
|
1779
|
-
}
|
|
1780
|
-
case "qwen-portal": {
|
|
1781
|
-
const { loginQwenPortal } = await import("./utils/oauth/qwen-portal");
|
|
1782
|
-
const apiKey = await loginQwenPortal(ctrl);
|
|
1783
|
-
await saveApiKeyCredential(apiKey);
|
|
1784
|
-
return;
|
|
1785
|
-
}
|
|
1786
|
-
case "nvidia": {
|
|
1787
|
-
const { loginNvidia } = await import("./utils/oauth/nvidia");
|
|
1788
|
-
const apiKey = await loginNvidia(ctrl);
|
|
1789
|
-
await saveApiKeyCredential(apiKey);
|
|
1790
|
-
return;
|
|
1791
|
-
}
|
|
1792
|
-
case "xiaomi": {
|
|
1793
|
-
const apiKey = await loginXiaomi(ctrl);
|
|
1794
|
-
await saveApiKeyCredential(apiKey);
|
|
1795
|
-
return;
|
|
1796
|
-
}
|
|
1797
|
-
case "xiaomi-token-plan-sgp": {
|
|
1798
|
-
const apiKey = await loginXiaomiTokenPlan(ctrl, "sgp");
|
|
1799
|
-
await saveApiKeyCredential(apiKey);
|
|
1800
|
-
return;
|
|
1801
|
-
}
|
|
1802
|
-
case "xiaomi-token-plan-ams": {
|
|
1803
|
-
const apiKey = await loginXiaomiTokenPlan(ctrl, "ams");
|
|
1804
|
-
await saveApiKeyCredential(apiKey);
|
|
1805
|
-
return;
|
|
1806
|
-
}
|
|
1807
|
-
case "xiaomi-token-plan-cn": {
|
|
1808
|
-
const apiKey = await loginXiaomiTokenPlan(ctrl, "cn");
|
|
1809
|
-
await saveApiKeyCredential(apiKey);
|
|
1810
|
-
return;
|
|
1811
|
-
}
|
|
1812
|
-
case "zenmux": {
|
|
1813
|
-
const { loginZenMux } = await import("./utils/oauth/zenmux");
|
|
1814
|
-
const apiKey = await loginZenMux(ctrl);
|
|
1815
|
-
await saveApiKeyCredential(apiKey);
|
|
1767
|
+
// Built-in registry first, then runtime-registered extension providers.
|
|
1768
|
+
const def = getProviderDefinition(provider) ?? getOAuthProvider(provider);
|
|
1769
|
+
if (!def?.login) {
|
|
1770
|
+
throw new Error(`Unknown OAuth provider: ${provider}`);
|
|
1771
|
+
}
|
|
1772
|
+
const result = await def.login({
|
|
1773
|
+
onAuth: ctrl.onAuth,
|
|
1774
|
+
onProgress: ctrl.onProgress,
|
|
1775
|
+
onPrompt: ctrl.onPrompt,
|
|
1776
|
+
onManualCodeInput: ctrl.onManualCodeInput ?? manualCodeInput,
|
|
1777
|
+
signal: ctrl.signal,
|
|
1778
|
+
fetch: ctrl.fetch,
|
|
1779
|
+
});
|
|
1780
|
+
if (typeof result === "string") {
|
|
1781
|
+
// Some flows (e.g. ollama) return "" to signal that no key was entered.
|
|
1782
|
+
if (!result) {
|
|
1816
1783
|
return;
|
|
1817
1784
|
}
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
if (!customProvider) {
|
|
1821
|
-
throw new Error(`Unknown OAuth provider: ${provider}`);
|
|
1822
|
-
}
|
|
1823
|
-
const customLoginResult = await customProvider.login({
|
|
1824
|
-
onAuth: info => ctrl.onAuth(info),
|
|
1825
|
-
onProgress: ctrl.onProgress,
|
|
1826
|
-
onPrompt: ctrl.onPrompt,
|
|
1827
|
-
onManualCodeInput: ctrl.onManualCodeInput ?? manualCodeInput,
|
|
1828
|
-
signal: ctrl.signal,
|
|
1829
|
-
});
|
|
1830
|
-
if (typeof customLoginResult === "string") {
|
|
1831
|
-
await saveApiKeyCredential(customLoginResult);
|
|
1832
|
-
return;
|
|
1833
|
-
}
|
|
1834
|
-
credentials = customLoginResult;
|
|
1835
|
-
break;
|
|
1836
|
-
}
|
|
1785
|
+
await saveApiKeyCredential(result);
|
|
1786
|
+
return;
|
|
1837
1787
|
}
|
|
1838
|
-
const newCredential: OAuthCredential = { type: "oauth", ...
|
|
1839
|
-
await this.#upsertOAuthCredential(provider, newCredential);
|
|
1788
|
+
const newCredential: OAuthCredential = { type: "oauth", ...result };
|
|
1789
|
+
await this.#upsertOAuthCredential(def.storeCredentialsAs ?? provider, newCredential);
|
|
1840
1790
|
}
|
|
1841
1791
|
|
|
1842
1792
|
/**
|
|
@@ -1896,15 +1846,19 @@ export class AuthStorage {
|
|
|
1896
1846
|
#buildUsageReportCacheKey(request: UsageRequestDescriptor): string {
|
|
1897
1847
|
const baseUrl = this.#normalizeUsageBaseUrl(request.baseUrl) || "default";
|
|
1898
1848
|
const identity = this.#buildUsageCacheIdentity(request.credential);
|
|
1899
|
-
|
|
1849
|
+
const versionOverride = USAGE_REPORT_CACHE_KEY_VERSION_OVERRIDES[request.provider];
|
|
1850
|
+
const providerKey = versionOverride === undefined ? request.provider : `${versionOverride}:${request.provider}`;
|
|
1851
|
+
return `report:${providerKey}:${baseUrl}:${identity}`;
|
|
1900
1852
|
}
|
|
1901
1853
|
|
|
1902
1854
|
#buildUsageReportsCacheKey(requests: ReadonlyArray<UsageRequestDescriptor>): string {
|
|
1903
1855
|
const snapshot = requests
|
|
1904
|
-
.map(
|
|
1905
|
-
request
|
|
1906
|
-
|
|
1907
|
-
|
|
1856
|
+
.map(request => {
|
|
1857
|
+
const versionOverride = USAGE_REPORT_CACHE_KEY_VERSION_OVERRIDES[request.provider];
|
|
1858
|
+
const providerKey =
|
|
1859
|
+
versionOverride === undefined ? request.provider : `${versionOverride}:${request.provider}`;
|
|
1860
|
+
return `${providerKey}:${this.#normalizeUsageBaseUrl(request.baseUrl) || "default"}:${this.#buildUsageCacheIdentity(request.credential)}`;
|
|
1861
|
+
})
|
|
1908
1862
|
.sort()
|
|
1909
1863
|
.join("\n");
|
|
1910
1864
|
return `reports:${Bun.hash(snapshot).toString(16)}`;
|
|
@@ -2146,6 +2100,7 @@ export class AuthStorage {
|
|
|
2146
2100
|
// fan-out trips 429s every cycle. With ±25% jitter on TTL the refresh
|
|
2147
2101
|
// times decorrelate within a few cycles.
|
|
2148
2102
|
this.#usageCache.set(cacheKey, { value: report, expiresAt: Date.now() + USAGE_REPORT_TTL_MS + ttlJitter });
|
|
2103
|
+
this.#recordUsageHistory(request, report);
|
|
2149
2104
|
return report;
|
|
2150
2105
|
}
|
|
2151
2106
|
// Failure: cache the LAST GOOD value (if any) with a short jittered TTL
|
|
@@ -2167,6 +2122,50 @@ export class AuthStorage {
|
|
|
2167
2122
|
return promise;
|
|
2168
2123
|
}
|
|
2169
2124
|
|
|
2125
|
+
/**
|
|
2126
|
+
* Append a freshly fetched report to durable usage history (when the store
|
|
2127
|
+
* supports it). The usage cache is latest-snapshot-only — these rows are
|
|
2128
|
+
* the only place limit utilization is kept over time.
|
|
2129
|
+
*/
|
|
2130
|
+
#recordUsageHistory(request: UsageRequestDescriptor, report: UsageReport): void {
|
|
2131
|
+
const record = this.#store.recordUsageSnapshots;
|
|
2132
|
+
if (!record || report.limits.length === 0) return;
|
|
2133
|
+
const recordedAt = Number.isFinite(report.fetchedAt) && report.fetchedAt > 0 ? report.fetchedAt : Date.now();
|
|
2134
|
+
const accountKey = this.#buildUsageCacheIdentity(request.credential);
|
|
2135
|
+
const metadata = report.metadata ?? {};
|
|
2136
|
+
const metaEmail = typeof metadata.email === "string" ? metadata.email : undefined;
|
|
2137
|
+
const metaAccountId = typeof metadata.accountId === "string" ? metadata.accountId : undefined;
|
|
2138
|
+
const entries: UsageHistoryEntry[] = report.limits.map(limit => ({
|
|
2139
|
+
recordedAt,
|
|
2140
|
+
provider: request.provider,
|
|
2141
|
+
accountKey,
|
|
2142
|
+
email: request.credential.email ?? metaEmail,
|
|
2143
|
+
accountId: request.credential.accountId ?? limit.scope.accountId ?? metaAccountId,
|
|
2144
|
+
limitId: limit.id,
|
|
2145
|
+
label: limit.label,
|
|
2146
|
+
windowLabel: limit.window?.label ?? limit.scope.windowId,
|
|
2147
|
+
usedFraction: resolveUsedFraction(limit),
|
|
2148
|
+
status: limit.status,
|
|
2149
|
+
resetsAt: limit.window?.resetsAt,
|
|
2150
|
+
}));
|
|
2151
|
+
try {
|
|
2152
|
+
record.call(this.#store, entries);
|
|
2153
|
+
} catch (error) {
|
|
2154
|
+
this.#usageLogger?.debug("usage history record failed", {
|
|
2155
|
+
provider: request.provider,
|
|
2156
|
+
error: String(error),
|
|
2157
|
+
});
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
/**
|
|
2162
|
+
* Recorded usage-limit snapshots, oldest first. Empty when the underlying
|
|
2163
|
+
* store has no durable history (e.g. a broker-backed remote store).
|
|
2164
|
+
*/
|
|
2165
|
+
listUsageHistory(query?: UsageHistoryQuery): UsageHistoryEntry[] {
|
|
2166
|
+
return this.#store.listUsageHistory?.(query) ?? [];
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2170
2169
|
ingestUsageHeaders(
|
|
2171
2170
|
provider: Provider,
|
|
2172
2171
|
headers: Record<string, string>,
|
|
@@ -2288,6 +2287,16 @@ export class AuthStorage {
|
|
|
2288
2287
|
return undefined;
|
|
2289
2288
|
}
|
|
2290
2289
|
|
|
2290
|
+
#getUsageReportScopeProjectId(report: UsageReport): string | undefined {
|
|
2291
|
+
const ids = new Set<string>();
|
|
2292
|
+
for (const limit of report.limits) {
|
|
2293
|
+
const projectId = limit.scope.projectId?.trim();
|
|
2294
|
+
if (projectId) ids.add(projectId);
|
|
2295
|
+
}
|
|
2296
|
+
if (ids.size === 1) return [...ids][0];
|
|
2297
|
+
return undefined;
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2291
2300
|
#getUsageReportIdentifiers(report: UsageReport): string[] {
|
|
2292
2301
|
const identifiers: string[] = [];
|
|
2293
2302
|
const email = this.#getUsageReportMetadataValue(report, "email");
|
|
@@ -2295,6 +2304,11 @@ export class AuthStorage {
|
|
|
2295
2304
|
if (report.provider === "openai-codex" || report.provider === "anthropic") {
|
|
2296
2305
|
return identifiers.map(identifier => `${report.provider}:${identifier.toLowerCase()}`);
|
|
2297
2306
|
}
|
|
2307
|
+
const projectId =
|
|
2308
|
+
this.#getUsageReportMetadataValue(report, "projectId") ?? this.#getUsageReportScopeProjectId(report);
|
|
2309
|
+
// Only add project as a fallback when no email is available — two users
|
|
2310
|
+
// with different emails on the same GCP project must not merge.
|
|
2311
|
+
if (projectId && !email) identifiers.push(`project:${projectId}`);
|
|
2298
2312
|
const accountId = this.#getUsageReportMetadataValue(report, "accountId");
|
|
2299
2313
|
if (accountId) identifiers.push(`account:${accountId}`);
|
|
2300
2314
|
const account = this.#getUsageReportMetadataValue(report, "account");
|
|
@@ -2391,15 +2405,24 @@ export class AuthStorage {
|
|
|
2391
2405
|
return false;
|
|
2392
2406
|
}
|
|
2393
2407
|
|
|
2408
|
+
/** Return the usage limits that apply to the requested model for this strategy. */
|
|
2409
|
+
#getScopedUsageLimits(
|
|
2410
|
+
strategy: CredentialRankingStrategy,
|
|
2411
|
+
report: UsageReport,
|
|
2412
|
+
context: CredentialRankingContext,
|
|
2413
|
+
): UsageLimit[] {
|
|
2414
|
+
return strategy.scopeLimits?.(report, context) ?? report.limits;
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2394
2417
|
/** Returns true if usage indicates rate limit has been reached. */
|
|
2395
|
-
#isUsageLimitReached(
|
|
2396
|
-
return
|
|
2418
|
+
#isUsageLimitReached(limits: UsageLimit[]): boolean {
|
|
2419
|
+
return limits.some(limit => this.#isUsageLimitExhausted(limit));
|
|
2397
2420
|
}
|
|
2398
2421
|
|
|
2399
2422
|
/** Extracts the earliest reset timestamp from exhausted windows (in ms). */
|
|
2400
|
-
#getUsageResetAtMs(
|
|
2423
|
+
#getUsageResetAtMs(limits: UsageLimit[], nowMs: number): number | undefined {
|
|
2401
2424
|
const candidates: number[] = [];
|
|
2402
|
-
for (const limit of
|
|
2425
|
+
for (const limit of limits) {
|
|
2403
2426
|
if (!this.#isUsageLimitExhausted(limit)) continue;
|
|
2404
2427
|
const window = limit.window;
|
|
2405
2428
|
if (window?.resetsAt && window.resetsAt > nowMs) {
|
|
@@ -2685,34 +2708,42 @@ export class AuthStorage {
|
|
|
2685
2708
|
/**
|
|
2686
2709
|
* Marks the current session's credential as temporarily blocked due to usage limits.
|
|
2687
2710
|
* Uses usage reports to determine accurate reset time when available.
|
|
2688
|
-
* Returns
|
|
2711
|
+
* Returns whether a sibling credential is available now; when none is, also
|
|
2712
|
+
* reports the earliest time a blocked sibling becomes available again so
|
|
2713
|
+
* callers can wait for the sibling instead of the provider's full window.
|
|
2689
2714
|
*/
|
|
2690
2715
|
async markUsageLimitReached(
|
|
2691
2716
|
provider: string,
|
|
2692
2717
|
sessionId: string | undefined,
|
|
2693
|
-
options?: { retryAfterMs?: number; baseUrl?: string; signal?: AbortSignal },
|
|
2694
|
-
): Promise<
|
|
2718
|
+
options?: { retryAfterMs?: number; baseUrl?: string; modelId?: string; signal?: AbortSignal },
|
|
2719
|
+
): Promise<UsageLimitMarkResult> {
|
|
2695
2720
|
const sessionCredential = this.#getSessionCredential(provider, sessionId);
|
|
2696
|
-
if (!sessionCredential) return false;
|
|
2721
|
+
if (!sessionCredential) return { switched: false };
|
|
2697
2722
|
|
|
2698
2723
|
const providerKey = this.#getProviderTypeKey(provider, sessionCredential.type);
|
|
2724
|
+
const strategy = this.#rankingStrategyResolver?.(provider);
|
|
2725
|
+
const rankingContext: CredentialRankingContext = { modelId: options?.modelId };
|
|
2726
|
+
const blockScope = strategy?.blockScope?.(rankingContext);
|
|
2699
2727
|
const now = Date.now();
|
|
2700
2728
|
let blockedUntil = now + (options?.retryAfterMs ?? AuthStorage.#defaultBackoffMs);
|
|
2701
2729
|
|
|
2702
|
-
if (sessionCredential.type === "oauth" &&
|
|
2730
|
+
if (sessionCredential.type === "oauth" && strategy) {
|
|
2703
2731
|
const credential = this.#getCredentialsForProvider(provider)[sessionCredential.index];
|
|
2704
2732
|
if (credential?.type === "oauth") {
|
|
2705
2733
|
const report = await this.#getUsageReport(provider, credential, options);
|
|
2706
|
-
if (report
|
|
2707
|
-
const
|
|
2708
|
-
if (
|
|
2709
|
-
|
|
2734
|
+
if (report) {
|
|
2735
|
+
const scopedLimits = this.#getScopedUsageLimits(strategy, report, rankingContext);
|
|
2736
|
+
if (this.#isUsageLimitReached(scopedLimits)) {
|
|
2737
|
+
const resetAtMs = this.#getUsageResetAtMs(scopedLimits, Date.now());
|
|
2738
|
+
if (resetAtMs && resetAtMs > blockedUntil) {
|
|
2739
|
+
blockedUntil = resetAtMs;
|
|
2740
|
+
}
|
|
2710
2741
|
}
|
|
2711
2742
|
}
|
|
2712
2743
|
}
|
|
2713
2744
|
}
|
|
2714
2745
|
|
|
2715
|
-
this.#markCredentialBlocked(providerKey, sessionCredential.index, blockedUntil);
|
|
2746
|
+
this.#markCredentialBlocked(providerKey, sessionCredential.index, blockedUntil, blockScope);
|
|
2716
2747
|
|
|
2717
2748
|
const remainingCredentials = this.#getCredentialsForProvider(provider)
|
|
2718
2749
|
.map((credential, index) => ({ credential, index }))
|
|
@@ -2721,7 +2752,13 @@ export class AuthStorage {
|
|
|
2721
2752
|
entry.credential.type === sessionCredential.type && entry.index !== sessionCredential.index,
|
|
2722
2753
|
);
|
|
2723
2754
|
|
|
2724
|
-
|
|
2755
|
+
let retryAtMs: number | undefined;
|
|
2756
|
+
for (const candidate of remainingCredentials) {
|
|
2757
|
+
const candidateBlockedUntil = this.#getCredentialBlockedUntil(providerKey, candidate.index, blockScope);
|
|
2758
|
+
if (candidateBlockedUntil === undefined) return { switched: true };
|
|
2759
|
+
if (retryAtMs === undefined || candidateBlockedUntil < retryAtMs) retryAtMs = candidateBlockedUntil;
|
|
2760
|
+
}
|
|
2761
|
+
return { switched: false, retryAtMs };
|
|
2725
2762
|
}
|
|
2726
2763
|
|
|
2727
2764
|
#resolveWindowResetAt(window: UsageLimit["window"]): number | undefined {
|
|
@@ -2781,12 +2818,14 @@ export class AuthStorage {
|
|
|
2781
2818
|
return left.planPriority - right.planPriority;
|
|
2782
2819
|
}
|
|
2783
2820
|
if (left.hasPriorityBoost !== right.hasPriorityBoost) return left.hasPriorityBoost ? -1 : 1;
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
if (
|
|
2788
|
-
|
|
2789
|
-
if (
|
|
2821
|
+
let metric = compareUsageRankingMetric(left.secondaryDrainRate, right.secondaryDrainRate);
|
|
2822
|
+
if (metric !== 0) return metric;
|
|
2823
|
+
metric = compareUsageRankingMetric(left.secondaryUsed, right.secondaryUsed);
|
|
2824
|
+
if (metric !== 0) return metric;
|
|
2825
|
+
metric = compareUsageRankingMetric(left.primaryDrainRate, right.primaryDrainRate);
|
|
2826
|
+
if (metric !== 0) return metric;
|
|
2827
|
+
metric = compareUsageRankingMetric(left.primaryUsed, right.primaryUsed);
|
|
2828
|
+
if (metric !== 0) return metric;
|
|
2790
2829
|
return 0;
|
|
2791
2830
|
}
|
|
2792
2831
|
|
|
@@ -2880,6 +2919,8 @@ export class AuthStorage {
|
|
|
2880
2919
|
options?: AuthApiKeyOptions;
|
|
2881
2920
|
sessionId?: string;
|
|
2882
2921
|
strategy: CredentialRankingStrategy;
|
|
2922
|
+
rankingContext: CredentialRankingContext;
|
|
2923
|
+
blockScope?: string;
|
|
2883
2924
|
}): Promise<OAuthCandidate[]> {
|
|
2884
2925
|
const nowMs = Date.now();
|
|
2885
2926
|
const { strategy } = args;
|
|
@@ -2893,7 +2934,7 @@ export class AuthStorage {
|
|
|
2893
2934
|
args.order.map(async idx => {
|
|
2894
2935
|
const selection = args.credentials[idx];
|
|
2895
2936
|
if (!selection) return null;
|
|
2896
|
-
const blockedUntil = this.#getCredentialBlockedUntil(args.providerKey, selection.index);
|
|
2937
|
+
const blockedUntil = this.#getCredentialBlockedUntil(args.providerKey, selection.index, args.blockScope);
|
|
2897
2938
|
if (blockedUntil !== undefined) return { selection, usage: null, usageChecked: false, blockedUntil };
|
|
2898
2939
|
const usage = await this.#getUsageReport(args.provider, selection.credential, {
|
|
2899
2940
|
...args.options,
|
|
@@ -2926,13 +2967,14 @@ export class AuthStorage {
|
|
|
2926
2967
|
const { selection, usage, usageChecked } = result;
|
|
2927
2968
|
let { blockedUntil } = result;
|
|
2928
2969
|
let blocked = blockedUntil !== undefined;
|
|
2929
|
-
|
|
2930
|
-
|
|
2970
|
+
const scopedLimits = usage ? this.#getScopedUsageLimits(strategy, usage, args.rankingContext) : undefined;
|
|
2971
|
+
if (!blocked && scopedLimits && this.#isUsageLimitReached(scopedLimits)) {
|
|
2972
|
+
const resetAtMs = this.#getUsageResetAtMs(scopedLimits, nowMs);
|
|
2931
2973
|
blockedUntil = resetAtMs ?? Date.now() + AuthStorage.#defaultBackoffMs;
|
|
2932
|
-
this.#markCredentialBlocked(args.providerKey, selection.index, blockedUntil);
|
|
2974
|
+
this.#markCredentialBlocked(args.providerKey, selection.index, blockedUntil, args.blockScope);
|
|
2933
2975
|
blocked = true;
|
|
2934
2976
|
}
|
|
2935
|
-
const windows = usage ? strategy.findWindowLimits(usage) : undefined;
|
|
2977
|
+
const windows = usage ? strategy.findWindowLimits(usage, args.rankingContext) : undefined;
|
|
2936
2978
|
const primary = windows?.primary;
|
|
2937
2979
|
const secondary = windows?.secondary;
|
|
2938
2980
|
const secondaryTarget = secondary ?? primary;
|
|
@@ -2981,6 +3023,8 @@ export class AuthStorage {
|
|
|
2981
3023
|
const providerKey = this.#getProviderTypeKey(provider, "oauth");
|
|
2982
3024
|
const order = this.#getCredentialOrder(providerKey, sessionId, credentials.length);
|
|
2983
3025
|
const strategy = this.#rankingStrategyResolver?.(provider);
|
|
3026
|
+
const rankingContext: CredentialRankingContext = { modelId: options?.modelId };
|
|
3027
|
+
const blockScope = strategy?.blockScope?.(rankingContext);
|
|
2984
3028
|
const requiresProModel = requiresOpenAICodexProModel(provider, options?.modelId);
|
|
2985
3029
|
const checkUsage = strategy !== undefined && (credentials.length > 1 || requiresProModel);
|
|
2986
3030
|
const sessionCredential = this.#getSessionCredential(provider, sessionId);
|
|
@@ -2990,7 +3034,8 @@ export class AuthStorage {
|
|
|
2990
3034
|
// (no preference) and sessions whose preferred is blocked still rank, so we pick the account
|
|
2991
3035
|
// with the most headroom proactively and fall back intelligently when rate-limited.
|
|
2992
3036
|
const sessionPreferredIsAvailable =
|
|
2993
|
-
sessionPreferredIndex !== undefined &&
|
|
3037
|
+
sessionPreferredIndex !== undefined &&
|
|
3038
|
+
!this.#isCredentialBlocked(providerKey, sessionPreferredIndex, blockScope);
|
|
2994
3039
|
const shouldRank = checkUsage && (!sessionPreferredIsAvailable || requiresProModel);
|
|
2995
3040
|
const rankingOrder = shouldRank && sessionId ? credentials.map((_credential, index) => index) : order;
|
|
2996
3041
|
const candidates = shouldRank
|
|
@@ -3002,6 +3047,8 @@ export class AuthStorage {
|
|
|
3002
3047
|
options,
|
|
3003
3048
|
sessionId,
|
|
3004
3049
|
strategy: strategy!,
|
|
3050
|
+
rankingContext,
|
|
3051
|
+
blockScope,
|
|
3005
3052
|
})
|
|
3006
3053
|
: order
|
|
3007
3054
|
.map(idx => credentials[idx])
|
|
@@ -3011,7 +3058,7 @@ export class AuthStorage {
|
|
|
3011
3058
|
if (sessionPreferredIndex !== undefined && !requiresProModel) {
|
|
3012
3059
|
const sessionPreferredCandidate = candidates.findIndex(
|
|
3013
3060
|
candidate =>
|
|
3014
|
-
!this.#isCredentialBlocked(providerKey, candidate.selection.index) &&
|
|
3061
|
+
!this.#isCredentialBlocked(providerKey, candidate.selection.index, blockScope) &&
|
|
3015
3062
|
candidate.selection.index === sessionPreferredIndex,
|
|
3016
3063
|
);
|
|
3017
3064
|
if (sessionPreferredCandidate > 0) {
|
|
@@ -3019,19 +3066,46 @@ export class AuthStorage {
|
|
|
3019
3066
|
candidates.unshift(preferred);
|
|
3020
3067
|
}
|
|
3021
3068
|
}
|
|
3069
|
+
// Step (b) of the auth-retry policy: when `forceRefresh` is set, re-mint
|
|
3070
|
+
// the session-preferred credential (or the first candidate when no
|
|
3071
|
+
// session preference exists yet) even if its cached token still looks
|
|
3072
|
+
// valid — a peer/broker may have rotated it out from under us.
|
|
3073
|
+
const forceRefreshIndex = options?.forceRefresh
|
|
3074
|
+
? (sessionPreferredIndex ?? candidates[0]?.selection.index)
|
|
3075
|
+
: undefined;
|
|
3022
3076
|
await Promise.all(
|
|
3023
3077
|
candidates.map(async candidate => {
|
|
3024
|
-
|
|
3078
|
+
const force = forceRefreshIndex !== undefined && candidate.selection.index === forceRefreshIndex;
|
|
3079
|
+
const initialCredentialId = this.#getStoredCredentials(provider)[candidate.selection.index]?.id;
|
|
3080
|
+
let syncedPeerCredential = false;
|
|
3081
|
+
if (initialCredentialId !== undefined) {
|
|
3082
|
+
const beforeSync = candidate.selection.credential;
|
|
3083
|
+
if (!this.#syncOAuthSelectionFromStore(provider, candidate.selection, initialCredentialId)) return;
|
|
3084
|
+
syncedPeerCredential = !authCredentialEquals(beforeSync, candidate.selection.credential);
|
|
3085
|
+
}
|
|
3086
|
+
const hasFreshAccess = Date.now() + OAUTH_REFRESH_SKEW_MS < candidate.selection.credential.expires;
|
|
3087
|
+
if ((!force || syncedPeerCredential) && hasFreshAccess) return;
|
|
3025
3088
|
const latestCredential = this.#getCredentialsForProvider(provider)[candidate.selection.index];
|
|
3026
|
-
if (
|
|
3089
|
+
if (
|
|
3090
|
+
!force &&
|
|
3091
|
+
latestCredential?.type === "oauth" &&
|
|
3092
|
+
Date.now() + OAUTH_REFRESH_SKEW_MS < latestCredential.expires
|
|
3093
|
+
) {
|
|
3027
3094
|
candidate.selection.credential = latestCredential;
|
|
3028
3095
|
return;
|
|
3029
3096
|
}
|
|
3030
3097
|
try {
|
|
3031
3098
|
const credentialId = this.#getStoredCredentials(provider)[candidate.selection.index]?.id;
|
|
3099
|
+
// Hand #refreshOAuthCredential a stale clone (expires:0) so its
|
|
3100
|
+
// not-yet-expired short-circuit doesn't suppress the forced
|
|
3101
|
+
// re-mint; an in-flight peer refresh is still awaited via the
|
|
3102
|
+
// per-credential single-flight.
|
|
3103
|
+
const refreshTarget = force
|
|
3104
|
+
? { ...candidate.selection.credential, expires: 0 }
|
|
3105
|
+
: candidate.selection.credential;
|
|
3032
3106
|
const refreshedCredentials = await this.#refreshOAuthCredential(
|
|
3033
3107
|
provider,
|
|
3034
|
-
|
|
3108
|
+
refreshTarget,
|
|
3035
3109
|
credentialId,
|
|
3036
3110
|
options?.signal,
|
|
3037
3111
|
);
|
|
@@ -3042,7 +3116,17 @@ export class AuthStorage {
|
|
|
3042
3116
|
};
|
|
3043
3117
|
candidate.selection.credential = updated;
|
|
3044
3118
|
this.#replaceCredentialAt(provider, candidate.selection.index, updated);
|
|
3045
|
-
} catch {
|
|
3119
|
+
} catch (error) {
|
|
3120
|
+
// Recovery for definitive failures (incl. peer rotation) lives in
|
|
3121
|
+
// #tryOAuthCredential; log instead of swallowing silently — a bare
|
|
3122
|
+
// catch here hid stale-refresh-token replays from concurrent
|
|
3123
|
+
// sessions (one-turn 401 "Invalid authentication credentials").
|
|
3124
|
+
logger.debug("OAuth preflight refresh failed", {
|
|
3125
|
+
provider,
|
|
3126
|
+
index: candidate.selection.index,
|
|
3127
|
+
error: String(error),
|
|
3128
|
+
});
|
|
3129
|
+
}
|
|
3046
3130
|
}),
|
|
3047
3131
|
);
|
|
3048
3132
|
|
|
@@ -3066,18 +3150,24 @@ export class AuthStorage {
|
|
|
3066
3150
|
prefetchedUsage: candidate.usage,
|
|
3067
3151
|
usagePrechecked: candidate.usageChecked,
|
|
3068
3152
|
enforceProRequirement,
|
|
3153
|
+
strategy,
|
|
3154
|
+
rankingContext,
|
|
3155
|
+
blockScope,
|
|
3069
3156
|
},
|
|
3070
3157
|
);
|
|
3071
3158
|
if (resolved) return resolved;
|
|
3072
3159
|
}
|
|
3073
3160
|
|
|
3074
|
-
if (fallback && this.#isCredentialBlocked(providerKey, fallback.selection.index)) {
|
|
3161
|
+
if (fallback && this.#isCredentialBlocked(providerKey, fallback.selection.index, blockScope)) {
|
|
3075
3162
|
return this.#tryOAuthCredential(provider, fallback.selection, providerKey, sessionId, options, {
|
|
3076
3163
|
checkUsage,
|
|
3077
3164
|
allowBlocked: true,
|
|
3078
3165
|
prefetchedUsage: fallback.usage,
|
|
3079
3166
|
usagePrechecked: fallback.usageChecked,
|
|
3080
3167
|
enforceProRequirement,
|
|
3168
|
+
strategy,
|
|
3169
|
+
rankingContext,
|
|
3170
|
+
blockScope,
|
|
3081
3171
|
});
|
|
3082
3172
|
}
|
|
3083
3173
|
|
|
@@ -3156,25 +3246,17 @@ export class AuthStorage {
|
|
|
3156
3246
|
}
|
|
3157
3247
|
}
|
|
3158
3248
|
|
|
3159
|
-
|
|
3249
|
+
#syncOAuthSelectionFromStore(
|
|
3160
3250
|
provider: string,
|
|
3161
3251
|
selection: { credential: OAuthCredential; index: number },
|
|
3162
|
-
|
|
3163
|
-
):
|
|
3164
|
-
const prepare = this.#store.prepareForRequest?.bind(this.#store);
|
|
3165
|
-
if (!prepare) return true;
|
|
3166
|
-
const stored = this.#getStoredCredentials(provider);
|
|
3167
|
-
const selected = stored[selection.index];
|
|
3168
|
-
if (selected?.credential.type !== "oauth") return false;
|
|
3169
|
-
|
|
3170
|
-
const prepared = await prepare(selected.id, { signal: options?.signal });
|
|
3171
|
-
if (!prepared) return true;
|
|
3252
|
+
credentialId: number,
|
|
3253
|
+
): boolean {
|
|
3172
3254
|
const latestRows = this.#store.listAuthCredentials(provider);
|
|
3173
3255
|
this.#setStoredCredentials(
|
|
3174
3256
|
provider,
|
|
3175
3257
|
latestRows.map(row => ({ id: row.id, credential: row.credential })),
|
|
3176
3258
|
);
|
|
3177
|
-
const latestIndex = latestRows.findIndex(row => row.id ===
|
|
3259
|
+
const latestIndex = latestRows.findIndex(row => row.id === credentialId);
|
|
3178
3260
|
if (latestIndex === -1) return false;
|
|
3179
3261
|
const latest = latestRows[latestIndex];
|
|
3180
3262
|
if (latest?.credential.type !== "oauth") return false;
|
|
@@ -3183,6 +3265,22 @@ export class AuthStorage {
|
|
|
3183
3265
|
return true;
|
|
3184
3266
|
}
|
|
3185
3267
|
|
|
3268
|
+
async #prepareOAuthCredentialForRequest(
|
|
3269
|
+
provider: string,
|
|
3270
|
+
selection: { credential: OAuthCredential; index: number },
|
|
3271
|
+
options: AuthApiKeyOptions | undefined,
|
|
3272
|
+
): Promise<boolean> {
|
|
3273
|
+
const stored = this.#getStoredCredentials(provider);
|
|
3274
|
+
const selected = stored[selection.index];
|
|
3275
|
+
if (selected?.credential.type !== "oauth") return false;
|
|
3276
|
+
|
|
3277
|
+
const prepare = this.#store.prepareForRequest?.bind(this.#store);
|
|
3278
|
+
if (prepare) {
|
|
3279
|
+
await prepare(selected.id, { signal: options?.signal });
|
|
3280
|
+
}
|
|
3281
|
+
return this.#syncOAuthSelectionFromStore(provider, selection, selected.id);
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3186
3284
|
/** Attempts to use a single OAuth credential, checking usage and refreshing token. */
|
|
3187
3285
|
async #tryOAuthCredential(
|
|
3188
3286
|
provider: Provider,
|
|
@@ -3196,6 +3294,9 @@ export class AuthStorage {
|
|
|
3196
3294
|
prefetchedUsage?: UsageReport | null;
|
|
3197
3295
|
usagePrechecked?: boolean;
|
|
3198
3296
|
enforceProRequirement?: boolean;
|
|
3297
|
+
strategy?: CredentialRankingStrategy;
|
|
3298
|
+
rankingContext?: CredentialRankingContext;
|
|
3299
|
+
blockScope?: string;
|
|
3199
3300
|
},
|
|
3200
3301
|
): Promise<OAuthResolutionResult | undefined> {
|
|
3201
3302
|
const {
|
|
@@ -3204,8 +3305,11 @@ export class AuthStorage {
|
|
|
3204
3305
|
prefetchedUsage = null,
|
|
3205
3306
|
usagePrechecked = false,
|
|
3206
3307
|
enforceProRequirement,
|
|
3308
|
+
strategy,
|
|
3309
|
+
rankingContext,
|
|
3310
|
+
blockScope,
|
|
3207
3311
|
} = usageOptions;
|
|
3208
|
-
if (!allowBlocked && this.#isCredentialBlocked(providerKey, selection.index)) {
|
|
3312
|
+
if (!allowBlocked && this.#isCredentialBlocked(providerKey, selection.index, blockScope)) {
|
|
3209
3313
|
return undefined;
|
|
3210
3314
|
}
|
|
3211
3315
|
|
|
@@ -3232,14 +3336,18 @@ export class AuthStorage {
|
|
|
3232
3336
|
if (applyProFilter && !hasOpenAICodexProPlan(usage)) {
|
|
3233
3337
|
return undefined;
|
|
3234
3338
|
}
|
|
3235
|
-
if (checkUsage && !allowBlocked && usage &&
|
|
3236
|
-
const
|
|
3237
|
-
this.#
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3339
|
+
if (checkUsage && !allowBlocked && usage && strategy && rankingContext) {
|
|
3340
|
+
const scopedLimits = this.#getScopedUsageLimits(strategy, usage, rankingContext);
|
|
3341
|
+
if (this.#isUsageLimitReached(scopedLimits)) {
|
|
3342
|
+
const resetAtMs = this.#getUsageResetAtMs(scopedLimits, Date.now());
|
|
3343
|
+
this.#markCredentialBlocked(
|
|
3344
|
+
providerKey,
|
|
3345
|
+
selection.index,
|
|
3346
|
+
resetAtMs ?? Date.now() + AuthStorage.#defaultBackoffMs,
|
|
3347
|
+
blockScope,
|
|
3348
|
+
);
|
|
3349
|
+
return undefined;
|
|
3350
|
+
}
|
|
3243
3351
|
}
|
|
3244
3352
|
}
|
|
3245
3353
|
|
|
@@ -3298,14 +3406,18 @@ export class AuthStorage {
|
|
|
3298
3406
|
if (applyProFilter && !hasOpenAICodexProPlan(usage)) {
|
|
3299
3407
|
return undefined;
|
|
3300
3408
|
}
|
|
3301
|
-
if (checkUsage && !allowBlocked && usage &&
|
|
3302
|
-
const
|
|
3303
|
-
this.#
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3409
|
+
if (checkUsage && !allowBlocked && usage && strategy && rankingContext) {
|
|
3410
|
+
const scopedLimits = this.#getScopedUsageLimits(strategy, usage, rankingContext);
|
|
3411
|
+
if (this.#isUsageLimitReached(scopedLimits)) {
|
|
3412
|
+
const resetAtMs = this.#getUsageResetAtMs(scopedLimits, Date.now());
|
|
3413
|
+
this.#markCredentialBlocked(
|
|
3414
|
+
providerKey,
|
|
3415
|
+
selection.index,
|
|
3416
|
+
resetAtMs ?? Date.now() + AuthStorage.#defaultBackoffMs,
|
|
3417
|
+
blockScope,
|
|
3418
|
+
);
|
|
3419
|
+
return undefined;
|
|
3420
|
+
}
|
|
3309
3421
|
}
|
|
3310
3422
|
}
|
|
3311
3423
|
this.#recordSessionCredential(provider, sessionId, "oauth", selection.index);
|
|
@@ -3575,6 +3687,151 @@ export class AuthStorage {
|
|
|
3575
3687
|
);
|
|
3576
3688
|
}
|
|
3577
3689
|
|
|
3690
|
+
/**
|
|
3691
|
+
* List saved rate-limit resets for every stored OAuth account of `provider`
|
|
3692
|
+
* (Codex), fetched LIVE from the dedicated `rate-limit-reset-credits` route.
|
|
3693
|
+
*
|
|
3694
|
+
* This deliberately bypasses the usage-report cache: `/wham/usage` is
|
|
3695
|
+
* IP-rate-limited and may serve stale (or pre-feature) snapshots when many
|
|
3696
|
+
* accounts are polled, which would hide redeemable credits. One entry per
|
|
3697
|
+
* account, with the session's active account flagged and unreachable
|
|
3698
|
+
* accounts carrying an `error`.
|
|
3699
|
+
*/
|
|
3700
|
+
async listResetCredits(options?: {
|
|
3701
|
+
provider?: string;
|
|
3702
|
+
sessionId?: string;
|
|
3703
|
+
baseUrlResolver?: (provider: string) => string | undefined;
|
|
3704
|
+
signal?: AbortSignal;
|
|
3705
|
+
}): Promise<ResetCreditAccountStatus[]> {
|
|
3706
|
+
const provider = options?.provider ?? "openai-codex";
|
|
3707
|
+
const accesses = await this.getOAuthAccesses(provider);
|
|
3708
|
+
if (accesses.length === 0) return [];
|
|
3709
|
+
const baseUrl = options?.baseUrlResolver?.(provider);
|
|
3710
|
+
const activeId = this.getOAuthAccountIdentity(provider, options?.sessionId);
|
|
3711
|
+
return Promise.all(
|
|
3712
|
+
accesses.map(async (access): Promise<ResetCreditAccountStatus> => {
|
|
3713
|
+
const active =
|
|
3714
|
+
!!activeId &&
|
|
3715
|
+
((!!activeId.accountId && activeId.accountId === access.accountId) ||
|
|
3716
|
+
(!!activeId.email && activeId.email === access.email));
|
|
3717
|
+
const base = {
|
|
3718
|
+
credentialId: access.credentialId,
|
|
3719
|
+
accountId: access.accountId,
|
|
3720
|
+
email: access.email,
|
|
3721
|
+
active,
|
|
3722
|
+
};
|
|
3723
|
+
if (!access.ok) return { ...base, availableCount: 0, credits: [], error: access.error };
|
|
3724
|
+
const list = await listCodexResetCredits({
|
|
3725
|
+
accessToken: access.accessToken,
|
|
3726
|
+
accountId: access.accountId,
|
|
3727
|
+
baseUrl,
|
|
3728
|
+
fetch: this.#usageFetch,
|
|
3729
|
+
signal: options?.signal,
|
|
3730
|
+
});
|
|
3731
|
+
if (!list) return { ...base, availableCount: 0, credits: [], error: "Failed to load saved resets" };
|
|
3732
|
+
return { ...base, availableCount: list.availableCount, credits: list.credits };
|
|
3733
|
+
}),
|
|
3734
|
+
);
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3737
|
+
/**
|
|
3738
|
+
* Redeem one saved rate-limit reset (OpenAI Codex "saved resets") for a
|
|
3739
|
+
* specific stored account.
|
|
3740
|
+
*
|
|
3741
|
+
* Resolves a fresh access token for the target account, picks an available
|
|
3742
|
+
* credit (the given `creditId`, else the first redeemable one), spends it,
|
|
3743
|
+
* and invalidates the cached usage report so the next `/usage` reflects the
|
|
3744
|
+
* reset. Never throws for business outcomes — inspect the returned `code`.
|
|
3745
|
+
*/
|
|
3746
|
+
async redeemResetCredit(options: {
|
|
3747
|
+
target: ResetCreditTarget;
|
|
3748
|
+
provider?: string;
|
|
3749
|
+
creditId?: string;
|
|
3750
|
+
baseUrlResolver?: (provider: string) => string | undefined;
|
|
3751
|
+
signal?: AbortSignal;
|
|
3752
|
+
}): Promise<ResetCreditRedeemOutcome> {
|
|
3753
|
+
const provider = options.provider ?? "openai-codex";
|
|
3754
|
+
const baseUrl = options.baseUrlResolver?.(provider);
|
|
3755
|
+
const { target } = options;
|
|
3756
|
+
const accesses = await this.getOAuthAccesses(provider);
|
|
3757
|
+
const match = accesses.find(
|
|
3758
|
+
access =>
|
|
3759
|
+
(target.credentialId !== undefined && access.credentialId === target.credentialId) ||
|
|
3760
|
+
(!!target.accountId && access.accountId === target.accountId) ||
|
|
3761
|
+
(!!target.email && access.email === target.email),
|
|
3762
|
+
);
|
|
3763
|
+
if (!match) return { ok: false, code: "no_account", accountId: target.accountId, email: target.email };
|
|
3764
|
+
if (!match.ok) {
|
|
3765
|
+
return { ok: false, code: "account_unavailable", accountId: match.accountId, email: match.email };
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3768
|
+
let creditId = options.creditId;
|
|
3769
|
+
if (!creditId) {
|
|
3770
|
+
const list = await listCodexResetCredits({
|
|
3771
|
+
accessToken: match.accessToken,
|
|
3772
|
+
accountId: match.accountId,
|
|
3773
|
+
baseUrl,
|
|
3774
|
+
fetch: this.#usageFetch,
|
|
3775
|
+
signal: options.signal,
|
|
3776
|
+
});
|
|
3777
|
+
const credit = list?.credits.find(entry => (entry.status ?? "available") === "available") ?? list?.credits[0];
|
|
3778
|
+
if (!credit) return { ok: false, code: "no_credit", accountId: match.accountId, email: match.email };
|
|
3779
|
+
creditId = credit.id;
|
|
3780
|
+
}
|
|
3781
|
+
|
|
3782
|
+
const result = await consumeCodexResetCredit({
|
|
3783
|
+
creditId,
|
|
3784
|
+
accessToken: match.accessToken,
|
|
3785
|
+
accountId: match.accountId,
|
|
3786
|
+
baseUrl,
|
|
3787
|
+
fetch: this.#usageFetch,
|
|
3788
|
+
signal: options.signal,
|
|
3789
|
+
});
|
|
3790
|
+
if (result.ok) {
|
|
3791
|
+
this.#invalidateUsageReportCache(provider, baseUrl);
|
|
3792
|
+
// The window this credential was blocked on (by markUsageLimitReached)
|
|
3793
|
+
// is now reset, so lift its temporary block — otherwise selection
|
|
3794
|
+
// keeps skipping/under-ranking the freshly-reset account.
|
|
3795
|
+
if (match.credentialId !== undefined) this.#clearCredentialBlocks(provider, match.credentialId);
|
|
3796
|
+
}
|
|
3797
|
+
return { ok: result.ok, code: result.code, accountId: match.accountId, email: match.email, creditId };
|
|
3798
|
+
}
|
|
3799
|
+
|
|
3800
|
+
/**
|
|
3801
|
+
* Force the next usage fetch for `provider` to bypass the 5-min cache, so
|
|
3802
|
+
* `/usage` reflects a freshly-redeemed reset instead of stale numbers.
|
|
3803
|
+
*/
|
|
3804
|
+
#invalidateUsageReportCache(provider: string, baseUrl?: string): void {
|
|
3805
|
+
const expired = Date.now() - 1;
|
|
3806
|
+
for (const entry of this.#getStoredCredentials(provider)) {
|
|
3807
|
+
if (entry.credential.type !== "oauth") continue;
|
|
3808
|
+
const cacheKey = this.#buildUsageReportCacheKey(
|
|
3809
|
+
this.#buildUsageRequestForOauth(provider, entry.credential, baseUrl),
|
|
3810
|
+
);
|
|
3811
|
+
const existing = this.#usageCache.getStale<UsageReport | null>(cacheKey);
|
|
3812
|
+
this.#usageCache.set(cacheKey, { value: existing?.value ?? null, expiresAt: expired });
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
/**
|
|
3817
|
+
* Lift any temporary backoff blocks on one credential (across the bare
|
|
3818
|
+
* `provider:oauth` key and its scoped `\0`-suffixed derivatives). Called
|
|
3819
|
+
* after a saved reset is redeemed so the just-reset account is immediately
|
|
3820
|
+
* selectable again instead of being skipped/under-ranked by a stale block
|
|
3821
|
+
* that `markUsageLimitReached` set for the now-obsolete reset time.
|
|
3822
|
+
*/
|
|
3823
|
+
#clearCredentialBlocks(provider: string, credentialId: number): void {
|
|
3824
|
+
const index = this.#getStoredCredentials(provider).findIndex(entry => entry.id === credentialId);
|
|
3825
|
+
if (index < 0) return;
|
|
3826
|
+
const providerKey = this.#getProviderTypeKey(provider, "oauth");
|
|
3827
|
+
const scopedPrefix = `${providerKey}\0`;
|
|
3828
|
+
for (const [key, backoffMap] of this.#credentialBackoff) {
|
|
3829
|
+
if (key !== providerKey && !key.startsWith(scopedPrefix)) continue;
|
|
3830
|
+
backoffMap.delete(index);
|
|
3831
|
+
if (backoffMap.size === 0) this.#credentialBackoff.delete(key);
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
|
|
3578
3835
|
#extractStructuredApiKeyToken(apiKey: string): string | undefined {
|
|
3579
3836
|
if (!apiKey.startsWith("{")) return undefined;
|
|
3580
3837
|
try {
|
|
@@ -3643,6 +3900,95 @@ export class AuthStorage {
|
|
|
3643
3900
|
return true;
|
|
3644
3901
|
}
|
|
3645
3902
|
|
|
3903
|
+
/**
|
|
3904
|
+
* Rotate away from the session's current credential after a retryable auth
|
|
3905
|
+
* error — step (c) of the auth-retry policy. Stateless: looks up the
|
|
3906
|
+
* session-sticky credential (no API-key matching needed), applies the
|
|
3907
|
+
* storage action for the error class, then clears the sticky so the next
|
|
3908
|
+
* {@link AuthStorage.getApiKey} for this session picks a sibling.
|
|
3909
|
+
*
|
|
3910
|
+
* - usage-limit / account-rate-limit error → {@link AuthStorage.markUsageLimitReached}
|
|
3911
|
+
* (temporary block via its own backoff — default plus server usage-report
|
|
3912
|
+
* reset; sticky left intact so the next resolve re-ranks around the block).
|
|
3913
|
+
* - otherwise (hard 401 / auth failure) → mark the credential suspect (or
|
|
3914
|
+
* reload when no broker hook is wired) and block it, then drop the sticky.
|
|
3915
|
+
*
|
|
3916
|
+
* Returns whether another usable credential of the same type remains.
|
|
3917
|
+
*/
|
|
3918
|
+
async rotateSessionCredential(
|
|
3919
|
+
provider: string,
|
|
3920
|
+
sessionId: string | undefined,
|
|
3921
|
+
options?: { error?: unknown; modelId?: string; signal?: AbortSignal },
|
|
3922
|
+
): Promise<boolean> {
|
|
3923
|
+
const sessionCredential = this.#getSessionCredential(provider, sessionId);
|
|
3924
|
+
if (!sessionCredential) return false;
|
|
3925
|
+
|
|
3926
|
+
const error = options?.error;
|
|
3927
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : undefined;
|
|
3928
|
+
if (message && isUsageLimitError(message)) {
|
|
3929
|
+
return (
|
|
3930
|
+
await this.markUsageLimitReached(provider, sessionId, {
|
|
3931
|
+
modelId: options?.modelId,
|
|
3932
|
+
signal: options?.signal,
|
|
3933
|
+
})
|
|
3934
|
+
).switched;
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3937
|
+
const providerKey = this.#getProviderTypeKey(provider, sessionCredential.type);
|
|
3938
|
+
// Snapshot sibling availability before mutating so a soft-deleting
|
|
3939
|
+
// suspect hook can't reindex the answer out from under us.
|
|
3940
|
+
const hasSibling = this.#getCredentialsForProvider(provider).some(
|
|
3941
|
+
(credential, index) =>
|
|
3942
|
+
credential.type === sessionCredential.type &&
|
|
3943
|
+
index !== sessionCredential.index &&
|
|
3944
|
+
!this.#isCredentialBlocked(providerKey, index),
|
|
3945
|
+
);
|
|
3946
|
+
const target = this.#getStoredCredentials(provider)[sessionCredential.index];
|
|
3947
|
+
this.#clearSessionCredential(provider, sessionId);
|
|
3948
|
+
this.#markCredentialBlocked(providerKey, sessionCredential.index, Date.now() + AuthStorage.#defaultBackoffMs);
|
|
3949
|
+
|
|
3950
|
+
if (target) {
|
|
3951
|
+
const markSuspect = this.#store.markCredentialSuspect?.bind(this.#store);
|
|
3952
|
+
if (markSuspect) {
|
|
3953
|
+
await markSuspect(target.id, { signal: options?.signal });
|
|
3954
|
+
} else {
|
|
3955
|
+
await this.reload();
|
|
3956
|
+
}
|
|
3957
|
+
const latestRows = this.#store.listAuthCredentials(provider);
|
|
3958
|
+
this.#setStoredCredentials(
|
|
3959
|
+
provider,
|
|
3960
|
+
latestRows.map(row => ({ id: row.id, credential: row.credential })),
|
|
3961
|
+
);
|
|
3962
|
+
}
|
|
3963
|
+
|
|
3964
|
+
return hasSibling;
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
/**
|
|
3968
|
+
* Build an {@link ApiKeyResolver} backed by this storage, implementing the
|
|
3969
|
+
* central a/b/c auth-retry policy:
|
|
3970
|
+
*
|
|
3971
|
+
* - initial (`error: undefined`) → resolve the session credential.
|
|
3972
|
+
* - step (b) `!lastChance` → force-refresh the SAME session-sticky credential.
|
|
3973
|
+
* - step (c) `lastChance` → rotate to a sibling credential, then re-resolve.
|
|
3974
|
+
*
|
|
3975
|
+
* Used by web-search providers and other consumers that hold an AuthStorage
|
|
3976
|
+
* directly (no ModelRegistry in scope).
|
|
3977
|
+
*/
|
|
3978
|
+
resolver(provider: string, options?: { sessionId?: string; baseUrl?: string; modelId?: string }): ApiKeyResolver {
|
|
3979
|
+
const { sessionId, baseUrl, modelId } = options ?? {};
|
|
3980
|
+
return async ({ lastChance, error, signal }) => {
|
|
3981
|
+
if (error === undefined) {
|
|
3982
|
+
return this.getApiKey(provider, sessionId, { baseUrl, modelId, signal });
|
|
3983
|
+
}
|
|
3984
|
+
if (lastChance) {
|
|
3985
|
+
await this.rotateSessionCredential(provider, sessionId, { error, modelId, signal });
|
|
3986
|
+
return this.getApiKey(provider, sessionId, { baseUrl, modelId, signal });
|
|
3987
|
+
}
|
|
3988
|
+
return this.getApiKey(provider, sessionId, { baseUrl, modelId, forceRefresh: true, signal });
|
|
3989
|
+
};
|
|
3990
|
+
}
|
|
3991
|
+
|
|
3646
3992
|
// ─── Auth Broker integration ────────────────────────────────────────────
|
|
3647
3993
|
|
|
3648
3994
|
/**
|
|
@@ -3861,6 +4207,17 @@ type SerializedCredentialRecord = {
|
|
|
3861
4207
|
const AUTH_SCHEMA_VERSION = 4;
|
|
3862
4208
|
const SQLITE_NOW_EPOCH = "CAST(strftime('%s','now') AS INTEGER)";
|
|
3863
4209
|
|
|
4210
|
+
/**
|
|
4211
|
+
* SQLite's busy result code family — base `SQLITE_BUSY` plus the extended
|
|
4212
|
+
* variants `SQLITE_BUSY_RECOVERY` (concurrent WAL recovery), `SQLITE_BUSY_SNAPSHOT`,
|
|
4213
|
+
* and `SQLITE_BUSY_TIMEOUT`. All warrant the same backoff-and-retry treatment.
|
|
4214
|
+
*/
|
|
4215
|
+
export function isSqliteBusyError(err: unknown): boolean {
|
|
4216
|
+
if (err === null || typeof err !== "object") return false;
|
|
4217
|
+
const code = (err as { code?: unknown }).code;
|
|
4218
|
+
return typeof code === "string" && code.startsWith("SQLITE_BUSY");
|
|
4219
|
+
}
|
|
4220
|
+
|
|
3864
4221
|
function normalizeStoredAccountId(accountId: string | null | undefined): string | null {
|
|
3865
4222
|
const normalized = accountId?.trim();
|
|
3866
4223
|
return normalized && normalized.length > 0 ? normalized : null;
|
|
@@ -3932,6 +4289,8 @@ function resolveProviderCredentialIdentityKey(provider: string, identifiers: str
|
|
|
3932
4289
|
const accountIdentifier = identifiers.find(identifier => identifier.startsWith("account:"));
|
|
3933
4290
|
if (accountIdentifier) return accountIdentifier;
|
|
3934
4291
|
if (emailIdentifier) return emailIdentifier;
|
|
4292
|
+
const projectIdentifier = identifiers.find(identifier => identifier.startsWith("project:"));
|
|
4293
|
+
if (projectIdentifier) return projectIdentifier;
|
|
3935
4294
|
return null;
|
|
3936
4295
|
}
|
|
3937
4296
|
|
|
@@ -3967,6 +4326,8 @@ function extractOAuthCredentialIdentifiers(credential: OAuthCredential): string[
|
|
|
3967
4326
|
if (accountId) identifiers.add(`account:${accountId}`);
|
|
3968
4327
|
const email = normalizeStoredEmail(credential.email);
|
|
3969
4328
|
if (email) identifiers.add(`email:${email}`);
|
|
4329
|
+
const projectId = normalizeStoredAccountId(credential.projectId);
|
|
4330
|
+
if (projectId) identifiers.add(`project:${projectId}`);
|
|
3970
4331
|
const accessIdentifiers = extractOAuthTokenIdentifiers(credential.access) ?? [];
|
|
3971
4332
|
for (const identifier of accessIdentifiers) {
|
|
3972
4333
|
identifiers.add(identifier);
|
|
@@ -4023,7 +4384,7 @@ function extractOAuthTokenIdentifiers(token: string | undefined): string[] | und
|
|
|
4023
4384
|
/**
|
|
4024
4385
|
* Default SQLite-backed implementation of {@link AuthCredentialStore}.
|
|
4025
4386
|
*
|
|
4026
|
-
* Used by the
|
|
4387
|
+
* Used by the Prometheus AI CLI and as the default store for `AuthStorage.create()`.
|
|
4027
4388
|
* Also exposes convenience methods (`saveOAuth`, `getOAuth`, `saveApiKey`,
|
|
4028
4389
|
* `getApiKey`, `listProviders`, `deleteProvider`) that callers can use directly
|
|
4029
4390
|
* without going through `AuthStorage`.
|
|
@@ -4043,6 +4404,10 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4043
4404
|
#getCacheIncludingExpiredStmt: Statement;
|
|
4044
4405
|
#upsertCacheStmt: Statement;
|
|
4045
4406
|
#deleteExpiredCacheStmt: Statement;
|
|
4407
|
+
#insertUsageHistoryStmt: Statement;
|
|
4408
|
+
#lastUsageHistoryStmt: Statement;
|
|
4409
|
+
#listUsageHistoryStmt: Statement;
|
|
4410
|
+
#updateUsageHistoryStmt: Statement;
|
|
4046
4411
|
#closed = false;
|
|
4047
4412
|
|
|
4048
4413
|
constructor(db: Database) {
|
|
@@ -4082,6 +4447,18 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4082
4447
|
"INSERT INTO cache (key, value, expires_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at",
|
|
4083
4448
|
);
|
|
4084
4449
|
this.#deleteExpiredCacheStmt = this.#db.prepare(`DELETE FROM cache WHERE expires_at <= ${SQLITE_NOW_EPOCH}`);
|
|
4450
|
+
this.#insertUsageHistoryStmt = this.#db.prepare(
|
|
4451
|
+
"INSERT INTO usage_history (recorded_at, provider, account_key, email, account_id, limit_id, label, window_label, used_fraction, status, resets_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
4452
|
+
);
|
|
4453
|
+
this.#lastUsageHistoryStmt = this.#db.prepare(
|
|
4454
|
+
"SELECT id, recorded_at FROM usage_history WHERE provider = ? AND account_key = ? AND limit_id = ? ORDER BY recorded_at DESC LIMIT 1",
|
|
4455
|
+
);
|
|
4456
|
+
this.#updateUsageHistoryStmt = this.#db.prepare(
|
|
4457
|
+
"UPDATE usage_history SET recorded_at = ?, email = ?, account_id = ?, label = ?, window_label = ?, used_fraction = ?, status = ?, resets_at = ? WHERE id = ?",
|
|
4458
|
+
);
|
|
4459
|
+
this.#listUsageHistoryStmt = this.#db.prepare(
|
|
4460
|
+
"SELECT recorded_at, provider, account_key, email, account_id, limit_id, label, window_label, used_fraction, status, resets_at FROM usage_history WHERE recorded_at >= ? AND (? IS NULL OR provider = ?) ORDER BY recorded_at ASC",
|
|
4461
|
+
);
|
|
4085
4462
|
}
|
|
4086
4463
|
|
|
4087
4464
|
static async open(dbPath: string = getAgentDbPath()): Promise<SqliteAuthCredentialStore> {
|
|
@@ -4094,21 +4471,49 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4094
4471
|
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
4095
4472
|
}
|
|
4096
4473
|
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4474
|
+
// Concurrent prometheus startups can race against WAL recovery and the schema
|
|
4475
|
+
// init's first lock-taking statement. Bun's default `busy_timeout` is 0,
|
|
4476
|
+
// so retry the open on `SQLITE_BUSY` / `SQLITE_BUSY_RECOVERY` with bounded
|
|
4477
|
+
// exponential backoff before surfacing the failure. See issue #2421.
|
|
4478
|
+
const maxAttempts = 4;
|
|
4479
|
+
const baseDelayMs = 100;
|
|
4480
|
+
let lastBusyError: Error | undefined;
|
|
4481
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
4482
|
+
let db: Database | undefined;
|
|
4483
|
+
try {
|
|
4484
|
+
db = new Database(dbPath);
|
|
4485
|
+
try {
|
|
4486
|
+
await fs.chmod(dbPath, 0o600);
|
|
4487
|
+
} catch {
|
|
4488
|
+
// Ignore chmod failures (e.g., Windows)
|
|
4489
|
+
}
|
|
4490
|
+
return new SqliteAuthCredentialStore(db);
|
|
4491
|
+
} catch (err) {
|
|
4492
|
+
db?.close();
|
|
4493
|
+
if (!isSqliteBusyError(err)) {
|
|
4494
|
+
throw err;
|
|
4495
|
+
}
|
|
4496
|
+
lastBusyError = err instanceof Error ? err : new Error(String(err));
|
|
4497
|
+
if (attempt < maxAttempts - 1) {
|
|
4498
|
+
await Bun.sleep(baseDelayMs * 2 ** attempt);
|
|
4499
|
+
}
|
|
4500
|
+
}
|
|
4102
4501
|
}
|
|
4103
|
-
|
|
4104
|
-
|
|
4502
|
+
throw new Error(
|
|
4503
|
+
`Failed to open auth database at '${dbPath}' after ${maxAttempts} attempts: ${lastBusyError?.message}`,
|
|
4504
|
+
{ cause: lastBusyError },
|
|
4505
|
+
);
|
|
4105
4506
|
}
|
|
4106
4507
|
|
|
4107
4508
|
#initializeSchema(): void {
|
|
4509
|
+
// Install the busy handler BEFORE any lock-taking statement (incl.
|
|
4510
|
+
// `PRAGMA journal_mode=WAL`, which acquires an exclusive lock during WAL
|
|
4511
|
+
// recovery). Without this, concurrent prometheus startups can crash here with
|
|
4512
|
+
// `SQLITE_BUSY` / `SQLITE_BUSY_RECOVERY`. See issue #2421.
|
|
4513
|
+
this.#db.run("PRAGMA busy_timeout = 5000");
|
|
4108
4514
|
this.#db.run(`
|
|
4109
4515
|
PRAGMA journal_mode=WAL;
|
|
4110
4516
|
PRAGMA synchronous=NORMAL;
|
|
4111
|
-
PRAGMA busy_timeout=5000;
|
|
4112
4517
|
CREATE TABLE IF NOT EXISTS auth_schema_version (
|
|
4113
4518
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
4114
4519
|
version INTEGER NOT NULL
|
|
@@ -4119,6 +4524,22 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4119
4524
|
expires_at INTEGER NOT NULL
|
|
4120
4525
|
);
|
|
4121
4526
|
CREATE INDEX IF NOT EXISTS idx_cache_expires ON cache(expires_at);
|
|
4527
|
+
CREATE TABLE IF NOT EXISTS usage_history (
|
|
4528
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
4529
|
+
recorded_at INTEGER NOT NULL,
|
|
4530
|
+
provider TEXT NOT NULL,
|
|
4531
|
+
account_key TEXT NOT NULL,
|
|
4532
|
+
email TEXT,
|
|
4533
|
+
account_id TEXT,
|
|
4534
|
+
limit_id TEXT NOT NULL,
|
|
4535
|
+
label TEXT NOT NULL,
|
|
4536
|
+
window_label TEXT,
|
|
4537
|
+
used_fraction REAL,
|
|
4538
|
+
status TEXT,
|
|
4539
|
+
resets_at INTEGER
|
|
4540
|
+
);
|
|
4541
|
+
CREATE INDEX IF NOT EXISTS idx_usage_history_series ON usage_history(provider, account_key, limit_id, recorded_at);
|
|
4542
|
+
CREATE INDEX IF NOT EXISTS idx_usage_history_recorded ON usage_history(recorded_at);
|
|
4122
4543
|
`);
|
|
4123
4544
|
|
|
4124
4545
|
if (!this.#authCredentialsTableExists()) {
|
|
@@ -4127,8 +4548,8 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4127
4548
|
return;
|
|
4128
4549
|
}
|
|
4129
4550
|
|
|
4130
|
-
const
|
|
4131
|
-
const
|
|
4551
|
+
const recordedVersion = this.#readAuthSchemaVersion();
|
|
4552
|
+
const schemaVersion = recordedVersion ?? this.#inferAuthSchemaVersion();
|
|
4132
4553
|
if (schemaVersion > AUTH_SCHEMA_VERSION) {
|
|
4133
4554
|
logger.warn("SqliteAuthCredentialStore schema version mismatch", {
|
|
4134
4555
|
current: schemaVersion,
|
|
@@ -4140,7 +4561,9 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4140
4561
|
|
|
4141
4562
|
this.#createAuthCredentialIndexes();
|
|
4142
4563
|
this.#backfillCredentialIdentityKeys();
|
|
4143
|
-
|
|
4564
|
+
// Rewriting an already-current version row is a no-op write transaction
|
|
4565
|
+
// on every boot; only persist when the recorded version actually changes.
|
|
4566
|
+
if (recordedVersion !== AUTH_SCHEMA_VERSION && schemaVersion <= AUTH_SCHEMA_VERSION) {
|
|
4144
4567
|
this.#writeAuthSchemaVersion(AUTH_SCHEMA_VERSION);
|
|
4145
4568
|
}
|
|
4146
4569
|
}
|
|
@@ -4296,9 +4719,13 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4296
4719
|
.all() as AuthRow[];
|
|
4297
4720
|
if (rows.length === 0) return;
|
|
4298
4721
|
|
|
4299
|
-
|
|
4722
|
+
let updateIdentity: Statement | null = null;
|
|
4300
4723
|
for (const row of rows) {
|
|
4301
4724
|
const identityKey = resolveRowCredentialIdentityKey(row.provider, row);
|
|
4725
|
+
// Rows whose identity cannot be derived stay NULL; writing NULL over
|
|
4726
|
+
// NULL would just burn a write transaction on every boot.
|
|
4727
|
+
if (identityKey === null) continue;
|
|
4728
|
+
updateIdentity ??= this.#db.prepare("UPDATE auth_credentials SET identity_key = ? WHERE id = ?");
|
|
4302
4729
|
updateIdentity.run(identityKey, row.id);
|
|
4303
4730
|
}
|
|
4304
4731
|
}
|
|
@@ -4520,6 +4947,80 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4520
4947
|
}
|
|
4521
4948
|
}
|
|
4522
4949
|
|
|
4950
|
+
recordUsageSnapshots(entries: UsageHistoryEntry[]): void {
|
|
4951
|
+
try {
|
|
4952
|
+
for (const entry of entries) {
|
|
4953
|
+
const bucket = Math.floor(entry.recordedAt / USAGE_HISTORY_BUCKET_MS);
|
|
4954
|
+
const last = this.#lastUsageHistoryStmt.get(entry.provider, entry.accountKey, entry.limitId) as
|
|
4955
|
+
| { id: number; recorded_at: number }
|
|
4956
|
+
| undefined;
|
|
4957
|
+
if (last && Math.floor(last.recorded_at / USAGE_HISTORY_BUCKET_MS) === bucket) {
|
|
4958
|
+
this.#updateUsageHistoryStmt.run(
|
|
4959
|
+
entry.recordedAt,
|
|
4960
|
+
entry.email ?? null,
|
|
4961
|
+
entry.accountId ?? null,
|
|
4962
|
+
entry.label,
|
|
4963
|
+
entry.windowLabel ?? null,
|
|
4964
|
+
entry.usedFraction ?? null,
|
|
4965
|
+
entry.status ?? null,
|
|
4966
|
+
entry.resetsAt ?? null,
|
|
4967
|
+
last.id,
|
|
4968
|
+
);
|
|
4969
|
+
continue;
|
|
4970
|
+
}
|
|
4971
|
+
this.#insertUsageHistoryStmt.run(
|
|
4972
|
+
entry.recordedAt,
|
|
4973
|
+
entry.provider,
|
|
4974
|
+
entry.accountKey,
|
|
4975
|
+
entry.email ?? null,
|
|
4976
|
+
entry.accountId ?? null,
|
|
4977
|
+
entry.limitId,
|
|
4978
|
+
entry.label,
|
|
4979
|
+
entry.windowLabel ?? null,
|
|
4980
|
+
entry.usedFraction ?? null,
|
|
4981
|
+
entry.status ?? null,
|
|
4982
|
+
entry.resetsAt ?? null,
|
|
4983
|
+
);
|
|
4984
|
+
}
|
|
4985
|
+
} catch {
|
|
4986
|
+
// History is best-effort; never break the usage fetch path.
|
|
4987
|
+
}
|
|
4988
|
+
}
|
|
4989
|
+
|
|
4990
|
+
listUsageHistory(query?: UsageHistoryQuery): UsageHistoryEntry[] {
|
|
4991
|
+
try {
|
|
4992
|
+
const provider = query?.provider ?? null;
|
|
4993
|
+
const rows = this.#listUsageHistoryStmt.all(query?.sinceMs ?? 0, provider, provider) as Array<{
|
|
4994
|
+
recorded_at: number;
|
|
4995
|
+
provider: string;
|
|
4996
|
+
account_key: string;
|
|
4997
|
+
email: string | null;
|
|
4998
|
+
account_id: string | null;
|
|
4999
|
+
limit_id: string;
|
|
5000
|
+
label: string;
|
|
5001
|
+
window_label: string | null;
|
|
5002
|
+
used_fraction: number | null;
|
|
5003
|
+
status: string | null;
|
|
5004
|
+
resets_at: number | null;
|
|
5005
|
+
}>;
|
|
5006
|
+
return rows.map(row => ({
|
|
5007
|
+
recordedAt: row.recorded_at,
|
|
5008
|
+
provider: row.provider as Provider,
|
|
5009
|
+
accountKey: row.account_key,
|
|
5010
|
+
email: row.email ?? undefined,
|
|
5011
|
+
accountId: row.account_id ?? undefined,
|
|
5012
|
+
limitId: row.limit_id,
|
|
5013
|
+
label: row.label,
|
|
5014
|
+
windowLabel: row.window_label ?? undefined,
|
|
5015
|
+
usedFraction: row.used_fraction ?? undefined,
|
|
5016
|
+
status: (row.status ?? undefined) as UsageHistoryEntry["status"],
|
|
5017
|
+
resetsAt: row.resets_at ?? undefined,
|
|
5018
|
+
}));
|
|
5019
|
+
} catch {
|
|
5020
|
+
return [];
|
|
5021
|
+
}
|
|
5022
|
+
}
|
|
5023
|
+
|
|
4523
5024
|
// ─── Convenience methods for CLI ────────────────────────────────────────
|
|
4524
5025
|
|
|
4525
5026
|
/**
|
|
@@ -4603,6 +5104,10 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4603
5104
|
this.#getCacheIncludingExpiredStmt.finalize();
|
|
4604
5105
|
this.#upsertCacheStmt.finalize();
|
|
4605
5106
|
this.#deleteExpiredCacheStmt.finalize();
|
|
5107
|
+
this.#insertUsageHistoryStmt.finalize();
|
|
5108
|
+
this.#lastUsageHistoryStmt.finalize();
|
|
5109
|
+
this.#listUsageHistoryStmt.finalize();
|
|
5110
|
+
this.#updateUsageHistoryStmt.finalize();
|
|
4606
5111
|
this.#db.close();
|
|
4607
5112
|
}
|
|
4608
5113
|
}
|