Fix Bedrock streaming and provider setup docs
- commit
- 7a7bf0b52c214d8b53198632fe46db94275f0ee3
- author
- patriceckhart · · 2026-06-05 06:31 UTC
- parent
- 498b769
- signature
- unsigned
- refs
- tag: v0.2.12
5 files · +393 -16
A docs/providers.md +210 -0
| @@ -0,0 +1,210 @@ | |||
| 1 | + | # zot providers | |
| 2 | + | ||
| 3 | + | zot ships with built-in providers and a model catalog. You can select models | |
| 4 | + | with `/model`, list them with `zot --list-models`, and add private models in | |
| 5 | + | `$ZOT_HOME/models.json`. | |
| 6 | + | ||
| 7 | + | ## Login methods | |
| 8 | + | ||
| 9 | + | Use `/login` in interactive mode. | |
| 10 | + | ||
| 11 | + | - `api key`: stores an API key in `$ZOT_HOME/auth.json` when the provider uses a normal key. | |
| 12 | + | - `subscription`: stores OAuth credentials for subscription-backed providers. | |
| 13 | + | ||
| 14 | + | Use `/logout` to remove stored credentials. | |
| 15 | + | ||
| 16 | + | Some providers need more than a single pasted key. For those providers, | |
| 17 | + | `/login` shows setup instructions instead of opening a localhost browser form. | |
| 18 | + | This avoids broken browser flows in SSH, containers, and `kubectl exec` | |
| 19 | + | sessions. | |
| 20 | + | ||
| 21 | + | Setup-instruction providers: | |
| 22 | + | ||
| 23 | + | - Amazon Bedrock | |
| 24 | + | - Google Vertex AI | |
| 25 | + | - Cloudflare Workers AI | |
| 26 | + | - Cloudflare AI Gateway | |
| 27 | + | - Azure OpenAI Responses | |
| 28 | + | ||
| 29 | + | ## Subscription providers | |
| 30 | + | ||
| 31 | + | These providers support subscription login: | |
| 32 | + | ||
| 33 | + | | Provider | Notes | | |
| 34 | + | | --- | --- | | |
| 35 | + | | Anthropic | Claude Pro/Max OAuth credentials. | | |
| 36 | + | | OpenAI Codex | ChatGPT Plus/Pro Codex subscription route. Separate from the OpenAI API-key provider. | | |
| 37 | + | | Kimi | Kimi subscription login. | | |
| 38 | + | | GitHub Copilot | GitHub Copilot token flow. | | |
| 39 | + | ||
| 40 | + | OAuth tokens are stored in `$ZOT_HOME/auth.json` and refreshed when refresh is | |
| 41 | + | available. | |
| 42 | + | ||
| 43 | + | ## API-key providers | |
| 44 | + | ||
| 45 | + | These providers can use environment variables. Simple API-key providers can | |
| 46 | + | also be configured through `/login`. Providers that require extra cloud setup | |
| 47 | + | show instructions and should be configured with environment variables. | |
| 48 | + | ||
| 49 | + | | Provider | Environment variable | Stored key | | |
| 50 | + | | --- | --- | --- | | |
| 51 | + | | Anthropic | `ANTHROPIC_API_KEY` | `anthropic` | | |
| 52 | + | | OpenAI | `OPENAI_API_KEY` | `openai` | | |
| 53 | + | | OpenAI Responses | `OPENAI_API_KEY` | `openai-responses` | | |
| 54 | + | | Kimi | `KIMI_API_KEY` or `MOONSHOT_API_KEY` | `kimi` | | |
| 55 | + | | Google Gemini | `GEMINI_API_KEY` or `GOOGLE_API_KEY` | `google` | | |
| 56 | + | | DeepSeek | `DEEPSEEK_API_KEY` | `deepseek` | | |
| 57 | + | | Moonshot AI | `MOONSHOT_API_KEY` | `moonshotai` | | |
| 58 | + | | Moonshot AI China | `MOONSHOT_API_KEY` | `moonshotai-cn` | | |
| 59 | + | | Groq | `GROQ_API_KEY` | `groq` | | |
| 60 | + | | xAI | `XAI_API_KEY` | `xai` | | |
| 61 | + | | Cerebras | `CEREBRAS_API_KEY` | `cerebras` | | |
| 62 | + | | Together AI | `TOGETHER_API_KEY` | `together` | | |
| 63 | + | | Hugging Face | `HF_TOKEN` | `huggingface` | | |
| 64 | + | | OpenRouter | `OPENROUTER_API_KEY` | `openrouter` | | |
| 65 | + | | Mistral | `MISTRAL_API_KEY` | `mistral` | | |
| 66 | + | | ZAI | `ZAI_API_KEY` | `zai` | | |
| 67 | + | | Xiaomi MiMo | `XIAOMI_API_KEY` | `xiaomi` | | |
| 68 | + | | Xiaomi Token Plan Amsterdam | `XIAOMI_TOKEN_PLAN_AMS_API_KEY` | `xiaomi-token-plan-ams` | | |
| 69 | + | | Xiaomi Token Plan China | `XIAOMI_TOKEN_PLAN_CN_API_KEY` | `xiaomi-token-plan-cn` | | |
| 70 | + | | Xiaomi Token Plan Singapore | `XIAOMI_TOKEN_PLAN_SGP_API_KEY` | `xiaomi-token-plan-sgp` | | |
| 71 | + | | MiniMax | `MINIMAX_API_KEY` | `minimax` | | |
| 72 | + | | MiniMax China | `MINIMAX_CN_API_KEY` or `MINIMAX_API_KEY` | `minimax-cn` | | |
| 73 | + | | Fireworks | `FIREWORKS_API_KEY` | `fireworks` | | |
| 74 | + | | Vercel AI Gateway | `AI_GATEWAY_API_KEY` | `vercel-ai-gateway` | | |
| 75 | + | | OpenCode Zen | `OPENCODE_API_KEY` | `opencode` | | |
| 76 | + | | OpenCode Go | `OPENCODE_API_KEY` | `opencode-go` | | |
| 77 | + | | GitHub Copilot token | `COPILOT_GITHUB_TOKEN` or `GITHUB_COPILOT_TOKEN` | `github-copilot` | | |
| 78 | + | | Cloudflare Workers AI | `CLOUDFLARE_API_KEY` | `cloudflare-workers-ai` | | |
| 79 | + | | Cloudflare AI Gateway | `CLOUDFLARE_API_KEY` | `cloudflare-ai-gateway` | | |
| 80 | + | | Azure OpenAI Responses | `AZURE_OPENAI_API_KEY` | `azure-openai-responses` | | |
| 81 | + | ||
| 82 | + | Example: | |
| 83 | + | ||
| 84 | + | ```bash | |
| 85 | + | export OPENROUTER_API_KEY=... | |
| 86 | + | zot --provider openrouter | |
| 87 | + | ``` | |
| 88 | + | ||
| 89 | + | ## Cloud providers | |
| 90 | + | ||
| 91 | + | ### Amazon Bedrock | |
| 92 | + | ||
| 93 | + | Bedrock is configured with AWS credentials, not a generic zot API-key entry. | |
| 94 | + | Use one of these credential sources: | |
| 95 | + | ||
| 96 | + | ```bash | |
| 97 | + | # AWS profile | |
| 98 | + | export AWS_PROFILE=your-profile | |
| 99 | + | ||
| 100 | + | # IAM access keys | |
| 101 | + | export AWS_ACCESS_KEY_ID=AKIA... | |
| 102 | + | export AWS_SECRET_ACCESS_KEY=... | |
| 103 | + | export AWS_SESSION_TOKEN=... # only for temporary credentials | |
| 104 | + | ||
| 105 | + | # Bedrock API key bearer token | |
| 106 | + | export AWS_BEARER_TOKEN_BEDROCK=bedrock-api-key-... | |
| 107 | + | ||
| 108 | + | # Region | |
| 109 | + | export AWS_REGION=us-east-1 | |
| 110 | + | ``` | |
| 111 | + | ||
| 112 | + | ECS task roles, IRSA, and other AWS SDK credential-chain sources are also | |
| 113 | + | supported. | |
| 114 | + | ||
| 115 | + | Example: | |
| 116 | + | ||
| 117 | + | ```bash | |
| 118 | + | AWS_BEARER_TOKEN_BEDROCK=bedrock-api-key-... AWS_REGION=us-east-1 \ | |
| 119 | + | zot --provider amazon-bedrock --model anthropic.claude-sonnet-4-5-20250929-v1:0 | |
| 120 | + | ``` | |
| 121 | + | ||
| 122 | + | Some Bedrock models require regional inference-profile IDs for on-demand | |
| 123 | + | throughput, such as `us.` or `eu.` prefixed model IDs. zot rewrites known | |
| 124 | + | families automatically where possible. Explicit profile IDs and ARNs are left | |
| 125 | + | unchanged. | |
| 126 | + | ||
| 127 | + | ### Google Vertex AI | |
| 128 | + | ||
| 129 | + | Vertex can use a Google API key when available: | |
| 130 | + | ||
| 131 | + | ```bash | |
| 132 | + | export GOOGLE_CLOUD_API_KEY=... | |
| 133 | + | zot --provider google-vertex | |
| 134 | + | ``` | |
| 135 | + | ||
| 136 | + | For service-account or application-default credentials, set the standard | |
| 137 | + | Google environment variables used by your deployment. | |
| 138 | + | ||
| 139 | + | ### Cloudflare AI Gateway | |
| 140 | + | ||
| 141 | + | Cloudflare AI Gateway needs a Cloudflare token plus account and gateway IDs: | |
| 142 | + | ||
| 143 | + | ```bash | |
| 144 | + | export CLOUDFLARE_API_KEY=... | |
| 145 | + | export CLOUDFLARE_ACCOUNT_ID=... | |
| 146 | + | export CLOUDFLARE_GATEWAY_ID=... | |
| 147 | + | zot --provider cloudflare-ai-gateway | |
| 148 | + | ``` | |
| 149 | + | ||
| 150 | + | ### Cloudflare Workers AI | |
| 151 | + | ||
| 152 | + | Workers AI needs a Cloudflare token and account ID: | |
| 153 | + | ||
| 154 | + | ```bash | |
| 155 | + | export CLOUDFLARE_API_KEY=... | |
| 156 | + | export CLOUDFLARE_ACCOUNT_ID=... | |
| 157 | + | zot --provider cloudflare-workers-ai | |
| 158 | + | ``` | |
| 159 | + | ||
| 160 | + | ### Azure OpenAI Responses | |
| 161 | + | ||
| 162 | + | ```bash | |
| 163 | + | export AZURE_OPENAI_API_KEY=... | |
| 164 | + | export AZURE_OPENAI_BASE_URL=https://your-resource.openai.azure.com | |
| 165 | + | export AZURE_OPENAI_API_VERSION=2024-02-01 # optional | |
| 166 | + | zot --provider azure-openai-responses | |
| 167 | + | ``` | |
| 168 | + | ||
| 169 | + | If your Azure deployment names differ from zot model IDs, add model overrides | |
| 170 | + | in `$ZOT_HOME/models.json`. | |
| 171 | + | ||
| 172 | + | ## Auth file | |
| 173 | + | ||
| 174 | + | Credentials are stored in `$ZOT_HOME/auth.json` with user-only permissions | |
| 175 | + | when zot creates the file. | |
| 176 | + | ||
| 177 | + | Example: | |
| 178 | + | ||
| 179 | + | ```json | |
| 180 | + | { | |
| 181 | + | "anthropic": { "api_key": "sk-ant-..." }, | |
| 182 | + | "openai": { "api_key": "sk-..." }, | |
| 183 | + | "google": { "api_key": "..." }, | |
| 184 | + | "additional_api_key_creds": { | |
| 185 | + | "openrouter": { "api_key": "..." }, | |
| 186 | + | "mistral": { "api_key": "..." } | |
| 187 | + | } | |
| 188 | + | } | |
| 189 | + | ``` | |
| 190 | + | ||
| 191 | + | The top-level keys are used for providers with dedicated credential fields. | |
| 192 | + | Other API-key providers are stored under `additional_api_key_creds`. Prefer | |
| 193 | + | `/login` so zot writes the correct schema. | |
| 194 | + | ||
| 195 | + | ## Custom providers and models | |
| 196 | + | ||
| 197 | + | Use `$ZOT_HOME/models.json` for private models, deployment aliases, local | |
| 198 | + | servers, or OpenAI-compatible gateways that are not in the built-in catalog. | |
| 199 | + | User entries override built-in entries with the same provider and model ID. | |
| 200 | + | ||
| 201 | + | ## Credential resolution | |
| 202 | + | ||
| 203 | + | For each request, zot checks credentials in this order: | |
| 204 | + | ||
| 205 | + | 1. Explicit CLI key, such as `--api-key`. | |
| 206 | + | 2. Provider-specific environment variables. | |
| 207 | + | 3. `$ZOT_HOME/auth.json`. | |
| 208 | + | 4. Custom provider credentials from `$ZOT_HOME/models.json`, when configured. | |
| 209 | + | ||
| 210 | + | Bedrock then uses the AWS SDK credential chain for the actual request. | |
| @@ -0,0 +1,210 @@ | |||||
| 1 | + | # zot providers | |||
| 2 | + | ||||
| 3 | + | zot ships with built-in providers and a model catalog. You can select models | |||
| 4 | + | with `/model`, list them with `zot --list-models`, and add private models in | |||
| 5 | + | `$ZOT_HOME/models.json`. | |||
| 6 | + | ||||
| 7 | + | ## Login methods | |||
| 8 | + | ||||
| 9 | + | Use `/login` in interactive mode. | |||
| 10 | + | ||||
| 11 | + | - `api key`: stores an API key in `$ZOT_HOME/auth.json` when the provider uses a normal key. | |||
| 12 | + | - `subscription`: stores OAuth credentials for subscription-backed providers. | |||
| 13 | + | ||||
| 14 | + | Use `/logout` to remove stored credentials. | |||
| 15 | + | ||||
| 16 | + | Some providers need more than a single pasted key. For those providers, | |||
| 17 | + | `/login` shows setup instructions instead of opening a localhost browser form. | |||
| 18 | + | This avoids broken browser flows in SSH, containers, and `kubectl exec` | |||
| 19 | + | sessions. | |||
| 20 | + | ||||
| 21 | + | Setup-instruction providers: | |||
| 22 | + | ||||
| 23 | + | - Amazon Bedrock | |||
| 24 | + | - Google Vertex AI | |||
| 25 | + | - Cloudflare Workers AI | |||
| 26 | + | - Cloudflare AI Gateway | |||
| 27 | + | - Azure OpenAI Responses | |||
| 28 | + | ||||
| 29 | + | ## Subscription providers | |||
| 30 | + | ||||
| 31 | + | These providers support subscription login: | |||
| 32 | + | ||||
| 33 | + | | Provider | Notes | | |||
| 34 | + | | --- | --- | | |||
| 35 | + | | Anthropic | Claude Pro/Max OAuth credentials. | | |||
| 36 | + | | OpenAI Codex | ChatGPT Plus/Pro Codex subscription route. Separate from the OpenAI API-key provider. | | |||
| 37 | + | | Kimi | Kimi subscription login. | | |||
| 38 | + | | GitHub Copilot | GitHub Copilot token flow. | | |||
| 39 | + | ||||
| 40 | + | OAuth tokens are stored in `$ZOT_HOME/auth.json` and refreshed when refresh is | |||
| 41 | + | available. | |||
| 42 | + | ||||
| 43 | + | ## API-key providers | |||
| 44 | + | ||||
| 45 | + | These providers can use environment variables. Simple API-key providers can | |||
| 46 | + | also be configured through `/login`. Providers that require extra cloud setup | |||
| 47 | + | show instructions and should be configured with environment variables. | |||
| 48 | + | ||||
| 49 | + | | Provider | Environment variable | Stored key | | |||
| 50 | + | | --- | --- | --- | | |||
| 51 | + | | Anthropic | `ANTHROPIC_API_KEY` | `anthropic` | | |||
| 52 | + | | OpenAI | `OPENAI_API_KEY` | `openai` | | |||
| 53 | + | | OpenAI Responses | `OPENAI_API_KEY` | `openai-responses` | | |||
| 54 | + | | Kimi | `KIMI_API_KEY` or `MOONSHOT_API_KEY` | `kimi` | | |||
| 55 | + | | Google Gemini | `GEMINI_API_KEY` or `GOOGLE_API_KEY` | `google` | | |||
| 56 | + | | DeepSeek | `DEEPSEEK_API_KEY` | `deepseek` | | |||
| 57 | + | | Moonshot AI | `MOONSHOT_API_KEY` | `moonshotai` | | |||
| 58 | + | | Moonshot AI China | `MOONSHOT_API_KEY` | `moonshotai-cn` | | |||
| 59 | + | | Groq | `GROQ_API_KEY` | `groq` | | |||
| 60 | + | | xAI | `XAI_API_KEY` | `xai` | | |||
| 61 | + | | Cerebras | `CEREBRAS_API_KEY` | `cerebras` | | |||
| 62 | + | | Together AI | `TOGETHER_API_KEY` | `together` | | |||
| 63 | + | | Hugging Face | `HF_TOKEN` | `huggingface` | | |||
| 64 | + | | OpenRouter | `OPENROUTER_API_KEY` | `openrouter` | | |||
| 65 | + | | Mistral | `MISTRAL_API_KEY` | `mistral` | | |||
| 66 | + | | ZAI | `ZAI_API_KEY` | `zai` | | |||
| 67 | + | | Xiaomi MiMo | `XIAOMI_API_KEY` | `xiaomi` | | |||
| 68 | + | | Xiaomi Token Plan Amsterdam | `XIAOMI_TOKEN_PLAN_AMS_API_KEY` | `xiaomi-token-plan-ams` | | |||
| 69 | + | | Xiaomi Token Plan China | `XIAOMI_TOKEN_PLAN_CN_API_KEY` | `xiaomi-token-plan-cn` | | |||
| 70 | + | | Xiaomi Token Plan Singapore | `XIAOMI_TOKEN_PLAN_SGP_API_KEY` | `xiaomi-token-plan-sgp` | | |||
| 71 | + | | MiniMax | `MINIMAX_API_KEY` | `minimax` | | |||
| 72 | + | | MiniMax China | `MINIMAX_CN_API_KEY` or `MINIMAX_API_KEY` | `minimax-cn` | | |||
| 73 | + | | Fireworks | `FIREWORKS_API_KEY` | `fireworks` | | |||
| 74 | + | | Vercel AI Gateway | `AI_GATEWAY_API_KEY` | `vercel-ai-gateway` | | |||
| 75 | + | | OpenCode Zen | `OPENCODE_API_KEY` | `opencode` | | |||
| 76 | + | | OpenCode Go | `OPENCODE_API_KEY` | `opencode-go` | | |||
| 77 | + | | GitHub Copilot token | `COPILOT_GITHUB_TOKEN` or `GITHUB_COPILOT_TOKEN` | `github-copilot` | | |||
| 78 | + | | Cloudflare Workers AI | `CLOUDFLARE_API_KEY` | `cloudflare-workers-ai` | | |||
| 79 | + | | Cloudflare AI Gateway | `CLOUDFLARE_API_KEY` | `cloudflare-ai-gateway` | | |||
| 80 | + | | Azure OpenAI Responses | `AZURE_OPENAI_API_KEY` | `azure-openai-responses` | | |||
| 81 | + | ||||
| 82 | + | Example: | |||
| 83 | + | ||||
| 84 | + | ```bash | |||
| 85 | + | export OPENROUTER_API_KEY=... | |||
| 86 | + | zot --provider openrouter | |||
| 87 | + | ``` | |||
| 88 | + | ||||
| 89 | + | ## Cloud providers | |||
| 90 | + | ||||
| 91 | + | ### Amazon Bedrock | |||
| 92 | + | ||||
| 93 | + | Bedrock is configured with AWS credentials, not a generic zot API-key entry. | |||
| 94 | + | Use one of these credential sources: | |||
| 95 | + | ||||
| 96 | + | ```bash | |||
| 97 | + | # AWS profile | |||
| 98 | + | export AWS_PROFILE=your-profile | |||
| 99 | + | ||||
| 100 | + | # IAM access keys | |||
| 101 | + | export AWS_ACCESS_KEY_ID=AKIA... | |||
| 102 | + | export AWS_SECRET_ACCESS_KEY=... | |||
| 103 | + | export AWS_SESSION_TOKEN=... # only for temporary credentials | |||
| 104 | + | ||||
| 105 | + | # Bedrock API key bearer token | |||
| 106 | + | export AWS_BEARER_TOKEN_BEDROCK=bedrock-api-key-... | |||
| 107 | + | ||||
| 108 | + | # Region | |||
| 109 | + | export AWS_REGION=us-east-1 | |||
| 110 | + | ``` | |||
| 111 | + | ||||
| 112 | + | ECS task roles, IRSA, and other AWS SDK credential-chain sources are also | |||
| 113 | + | supported. | |||
| 114 | + | ||||
| 115 | + | Example: | |||
| 116 | + | ||||
| 117 | + | ```bash | |||
| 118 | + | AWS_BEARER_TOKEN_BEDROCK=bedrock-api-key-... AWS_REGION=us-east-1 \ | |||
| 119 | + | zot --provider amazon-bedrock --model anthropic.claude-sonnet-4-5-20250929-v1:0 | |||
| 120 | + | ``` | |||
| 121 | + | ||||
| 122 | + | Some Bedrock models require regional inference-profile IDs for on-demand | |||
| 123 | + | throughput, such as `us.` or `eu.` prefixed model IDs. zot rewrites known | |||
| 124 | + | families automatically where possible. Explicit profile IDs and ARNs are left | |||
| 125 | + | unchanged. | |||
| 126 | + | ||||
| 127 | + | ### Google Vertex AI | |||
| 128 | + | ||||
| 129 | + | Vertex can use a Google API key when available: | |||
| 130 | + | ||||
| 131 | + | ```bash | |||
| 132 | + | export GOOGLE_CLOUD_API_KEY=... | |||
| 133 | + | zot --provider google-vertex | |||
| 134 | + | ``` | |||
| 135 | + | ||||
| 136 | + | For service-account or application-default credentials, set the standard | |||
| 137 | + | Google environment variables used by your deployment. | |||
| 138 | + | ||||
| 139 | + | ### Cloudflare AI Gateway | |||
| 140 | + | ||||
| 141 | + | Cloudflare AI Gateway needs a Cloudflare token plus account and gateway IDs: | |||
| 142 | + | ||||
| 143 | + | ```bash | |||
| 144 | + | export CLOUDFLARE_API_KEY=... | |||
| 145 | + | export CLOUDFLARE_ACCOUNT_ID=... | |||
| 146 | + | export CLOUDFLARE_GATEWAY_ID=... | |||
| 147 | + | zot --provider cloudflare-ai-gateway | |||
| 148 | + | ``` | |||
| 149 | + | ||||
| 150 | + | ### Cloudflare Workers AI | |||
| 151 | + | ||||
| 152 | + | Workers AI needs a Cloudflare token and account ID: | |||
| 153 | + | ||||
| 154 | + | ```bash | |||
| 155 | + | export CLOUDFLARE_API_KEY=... | |||
| 156 | + | export CLOUDFLARE_ACCOUNT_ID=... | |||
| 157 | + | zot --provider cloudflare-workers-ai | |||
| 158 | + | ``` | |||
| 159 | + | ||||
| 160 | + | ### Azure OpenAI Responses | |||
| 161 | + | ||||
| 162 | + | ```bash | |||
| 163 | + | export AZURE_OPENAI_API_KEY=... | |||
| 164 | + | export AZURE_OPENAI_BASE_URL=https://your-resource.openai.azure.com | |||
| 165 | + | export AZURE_OPENAI_API_VERSION=2024-02-01 # optional | |||
| 166 | + | zot --provider azure-openai-responses | |||
| 167 | + | ``` | |||
| 168 | + | ||||
| 169 | + | If your Azure deployment names differ from zot model IDs, add model overrides | |||
| 170 | + | in `$ZOT_HOME/models.json`. | |||
| 171 | + | ||||
| 172 | + | ## Auth file | |||
| 173 | + | ||||
| 174 | + | Credentials are stored in `$ZOT_HOME/auth.json` with user-only permissions | |||
| 175 | + | when zot creates the file. | |||
| 176 | + | ||||
| 177 | + | Example: | |||
| 178 | + | ||||
| 179 | + | ```json | |||
| 180 | + | { | |||
| 181 | + | "anthropic": { "api_key": "sk-ant-..." }, | |||
| 182 | + | "openai": { "api_key": "sk-..." }, | |||
| 183 | + | "google": { "api_key": "..." }, | |||
| 184 | + | "additional_api_key_creds": { | |||
| 185 | + | "openrouter": { "api_key": "..." }, | |||
| 186 | + | "mistral": { "api_key": "..." } | |||
| 187 | + | } | |||
| 188 | + | } | |||
| 189 | + | ``` | |||
| 190 | + | ||||
| 191 | + | The top-level keys are used for providers with dedicated credential fields. | |||
| 192 | + | Other API-key providers are stored under `additional_api_key_creds`. Prefer | |||
| 193 | + | `/login` so zot writes the correct schema. | |||
| 194 | + | ||||
| 195 | + | ## Custom providers and models | |||
| 196 | + | ||||
| 197 | + | Use `$ZOT_HOME/models.json` for private models, deployment aliases, local | |||
| 198 | + | servers, or OpenAI-compatible gateways that are not in the built-in catalog. | |||
| 199 | + | User entries override built-in entries with the same provider and model ID. | |||
| 200 | + | ||||
| 201 | + | ## Credential resolution | |||
| 202 | + | ||||
| 203 | + | For each request, zot checks credentials in this order: | |||
| 204 | + | ||||
| 205 | + | 1. Explicit CLI key, such as `--api-key`. | |||
| 206 | + | 2. Provider-specific environment variables. | |||
| 207 | + | 3. `$ZOT_HOME/auth.json`. | |||
| 208 | + | 4. Custom provider credentials from `$ZOT_HOME/models.json`, when configured. | |||
| 209 | + | ||||
| 210 | + | Bedrock then uses the AWS SDK credential chain for the actual request. | |||
M packages/agent/modes/interactive.go +72 -0
| @@ -3405,7 +3405,79 @@ func (i *Interactive) doLogout(target string) { | |||
| 3405 | 3405 | } | |
| 3406 | 3406 | } | |
| 3407 | 3407 | ||
| 3408 | + | func providerSetupInfo(provider string) (string, []string, bool) { | |
| 3409 | + | const docsURL = "https://raw.githubusercontent.com/patriceckhart/zot/main/docs/providers.md" | |
| 3410 | + | switch provider { | |
| 3411 | + | case "amazon-bedrock": | |
| 3412 | + | return "Amazon Bedrock setup", []string{ | |
| 3413 | + | "Amazon Bedrock uses AWS credentials instead of a generic zot API-key entry.", | |
| 3414 | + | "Configure an AWS profile, IAM keys, bearer token, or role-based credentials.", | |
| 3415 | + | "", | |
| 3416 | + | "For Bedrock API keys, set:", | |
| 3417 | + | " AWS_BEARER_TOKEN_BEDROCK=...", | |
| 3418 | + | " AWS_REGION=us-east-1", | |
| 3419 | + | "", | |
| 3420 | + | "Docs:", | |
| 3421 | + | " " + docsURL, | |
| 3422 | + | }, true | |
| 3423 | + | case "google-vertex": | |
| 3424 | + | return "Google Vertex AI setup", []string{ | |
| 3425 | + | "Google Vertex AI usually uses Google Cloud credentials and project settings.", | |
| 3426 | + | "Set a Google API key, application-default credentials, or a service account.", | |
| 3427 | + | "", | |
| 3428 | + | "Common environment:", | |
| 3429 | + | " GOOGLE_CLOUD_API_KEY=...", | |
| 3430 | + | " GOOGLE_CLOUD_PROJECT=...", | |
| 3431 | + | " GOOGLE_CLOUD_LOCATION=us-central1", | |
| 3432 | + | "", | |
| 3433 | + | "Docs:", | |
| 3434 | + | " " + docsURL, | |
| 3435 | + | }, true | |
| 3436 | + | case "cloudflare-workers-ai": | |
| 3437 | + | return "Cloudflare Workers AI setup", []string{ | |
| 3438 | + | "Cloudflare Workers AI needs both an API token and an account ID.", | |
| 3439 | + | "", | |
| 3440 | + | "Set:", | |
| 3441 | + | " CLOUDFLARE_API_KEY=...", | |
| 3442 | + | " CLOUDFLARE_ACCOUNT_ID=...", | |
| 3443 | + | "", | |
| 3444 | + | "Docs:", | |
| 3445 | + | " " + docsURL, | |
| 3446 | + | }, true | |
| 3447 | + | case "cloudflare-ai-gateway": | |
| 3448 | + | return "Cloudflare AI Gateway setup", []string{ | |
| 3449 | + | "Cloudflare AI Gateway needs an API token, account ID, and gateway ID.", | |
| 3450 | + | "", | |
| 3451 | + | "Set:", | |
| 3452 | + | " CLOUDFLARE_API_KEY=...", | |
| 3453 | + | " CLOUDFLARE_ACCOUNT_ID=...", | |
| 3454 | + | " CLOUDFLARE_GATEWAY_ID=...", | |
| 3455 | + | "", | |
| 3456 | + | "Docs:", | |
| 3457 | + | " " + docsURL, | |
| 3458 | + | }, true | |
| 3459 | + | case "azure-openai-responses": | |
| 3460 | + | return "Azure OpenAI Responses setup", []string{ | |
| 3461 | + | "Azure OpenAI needs an API key plus your Azure endpoint or deployment setup.", | |
| 3462 | + | "", | |
| 3463 | + | "Set:", | |
| 3464 | + | " AZURE_OPENAI_API_KEY=...", | |
| 3465 | + | " AZURE_OPENAI_BASE_URL=https://your-resource.openai.azure.com", | |
| 3466 | + | " AZURE_OPENAI_API_VERSION=2024-02-01", | |
| 3467 | + | "", | |
| 3468 | + | "Docs:", | |
| 3469 | + | " " + docsURL, | |
| 3470 | + | }, true | |
| 3471 | + | default: | |
| 3472 | + | return "", nil, false | |
| 3473 | + | } | |
| 3474 | + | } | |
| 3475 | + | ||
| 3408 | 3476 | func (i *Interactive) startAPIKeyFlow(provider string) { | |
| 3477 | + | if title, lines, ok := providerSetupInfo(provider); ok { | |
| 3478 | + | i.dialog.ShowInfo(title, lines) | |
| 3479 | + | return | |
| 3480 | + | } | |
| 3409 | 3481 | if provider == "kimi" && i.cfg.SetKimiCLIFallbackDisabled != nil { | |
| 3410 | 3482 | _ = i.cfg.SetKimiCLIFallbackDisabled(false) | |
| 3411 | 3483 | } | |
| @@ -3405,7 +3405,79 @@ func (i *Interactive) doLogout(target string) { | |||||
| 3405 | } | 3405 | } | ||
| 3406 | } | 3406 | } | ||
| 3407 | 3407 | ||||
| 3408 | + | func providerSetupInfo(provider string) (string, []string, bool) { | |||
| 3409 | + | const docsURL = "https://raw.githubusercontent.com/patriceckhart/zot/main/docs/providers.md" | |||
| 3410 | + | switch provider { | |||
| 3411 | + | case "amazon-bedrock": | |||
| 3412 | + | return "Amazon Bedrock setup", []string{ | |||
| 3413 | + | "Amazon Bedrock uses AWS credentials instead of a generic zot API-key entry.", | |||
| 3414 | + | "Configure an AWS profile, IAM keys, bearer token, or role-based credentials.", | |||
| 3415 | + | "", | |||
| 3416 | + | "For Bedrock API keys, set:", | |||
| 3417 | + | " AWS_BEARER_TOKEN_BEDROCK=...", | |||
| 3418 | + | " AWS_REGION=us-east-1", | |||
| 3419 | + | "", | |||
| 3420 | + | "Docs:", | |||
| 3421 | + | " " + docsURL, | |||
| 3422 | + | }, true | |||
| 3423 | + | case "google-vertex": | |||
| 3424 | + | return "Google Vertex AI setup", []string{ | |||
| 3425 | + | "Google Vertex AI usually uses Google Cloud credentials and project settings.", | |||
| 3426 | + | "Set a Google API key, application-default credentials, or a service account.", | |||
| 3427 | + | "", | |||
| 3428 | + | "Common environment:", | |||
| 3429 | + | " GOOGLE_CLOUD_API_KEY=...", | |||
| 3430 | + | " GOOGLE_CLOUD_PROJECT=...", | |||
| 3431 | + | " GOOGLE_CLOUD_LOCATION=us-central1", | |||
| 3432 | + | "", | |||
| 3433 | + | "Docs:", | |||
| 3434 | + | " " + docsURL, | |||
| 3435 | + | }, true | |||
| 3436 | + | case "cloudflare-workers-ai": | |||
| 3437 | + | return "Cloudflare Workers AI setup", []string{ | |||
| 3438 | + | "Cloudflare Workers AI needs both an API token and an account ID.", | |||
| 3439 | + | "", | |||
| 3440 | + | "Set:", | |||
| 3441 | + | " CLOUDFLARE_API_KEY=...", | |||
| 3442 | + | " CLOUDFLARE_ACCOUNT_ID=...", | |||
| 3443 | + | "", | |||
| 3444 | + | "Docs:", | |||
| 3445 | + | " " + docsURL, | |||
| 3446 | + | }, true | |||
| 3447 | + | case "cloudflare-ai-gateway": | |||
| 3448 | + | return "Cloudflare AI Gateway setup", []string{ | |||
| 3449 | + | "Cloudflare AI Gateway needs an API token, account ID, and gateway ID.", | |||
| 3450 | + | "", | |||
| 3451 | + | "Set:", | |||
| 3452 | + | " CLOUDFLARE_API_KEY=...", | |||
| 3453 | + | " CLOUDFLARE_ACCOUNT_ID=...", | |||
| 3454 | + | " CLOUDFLARE_GATEWAY_ID=...", | |||
| 3455 | + | "", | |||
| 3456 | + | "Docs:", | |||
| 3457 | + | " " + docsURL, | |||
| 3458 | + | }, true | |||
| 3459 | + | case "azure-openai-responses": | |||
| 3460 | + | return "Azure OpenAI Responses setup", []string{ | |||
| 3461 | + | "Azure OpenAI needs an API key plus your Azure endpoint or deployment setup.", | |||
| 3462 | + | "", | |||
| 3463 | + | "Set:", | |||
| 3464 | + | " AZURE_OPENAI_API_KEY=...", | |||
| 3465 | + | " AZURE_OPENAI_BASE_URL=https://your-resource.openai.azure.com", | |||
| 3466 | + | " AZURE_OPENAI_API_VERSION=2024-02-01", | |||
| 3467 | + | "", | |||
| 3468 | + | "Docs:", | |||
| 3469 | + | " " + docsURL, | |||
| 3470 | + | }, true | |||
| 3471 | + | default: | |||
| 3472 | + | return "", nil, false | |||
| 3473 | + | } | |||
| 3474 | + | } | |||
| 3475 | + | ||||
| 3408 | func (i *Interactive) startAPIKeyFlow(provider string) { | 3476 | func (i *Interactive) startAPIKeyFlow(provider string) { | ||
| 3477 | + | if title, lines, ok := providerSetupInfo(provider); ok { | |||
| 3478 | + | i.dialog.ShowInfo(title, lines) | |||
| 3479 | + | return | |||
| 3480 | + | } | |||
| 3409 | if provider == "kimi" && i.cfg.SetKimiCLIFallbackDisabled != nil { | 3481 | if provider == "kimi" && i.cfg.SetKimiCLIFallbackDisabled != nil { | ||
| 3410 | _ = i.cfg.SetKimiCLIFallbackDisabled(false) | 3482 | _ = i.cfg.SetKimiCLIFallbackDisabled(false) | ||
| 3411 | } | 3483 | } | ||
M packages/agent/modes/login_dialog.go +48 -10
| @@ -3,6 +3,7 @@ package modes | |||
| 3 | 3 | import ( | |
| 4 | 4 | "fmt" | |
| 5 | 5 | "path/filepath" | |
| 6 | + | "sort" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/patriceckhart/zot/packages/provider" | |
| 8 | 9 | "github.com/patriceckhart/zot/packages/provider/auth" | |
| @@ -21,6 +22,7 @@ const ( | |||
| 21 | 22 | loginStepProvider // pick anthropic vs openai vs kimi | |
| 22 | 23 | loginStepWaiting // browser open, waiting for callback | |
| 23 | 24 | loginStepPasteCode // user pastes the auth code here | |
| 25 | + | loginStepInfo // informational setup guidance | |
| 24 | 26 | loginStepDone // success or error, waiting for key to dismiss | |
| 25 | 27 | ) | |
| 26 | 28 | ||
| @@ -29,14 +31,16 @@ const loginProviderPageSize = 8 | |||
| 29 | 31 | // loginDialog is a tiny inline dialog rendered above the editor while | |
| 30 | 32 | // the user picks their login method and provider. | |
| 31 | 33 | type loginDialog struct { | |
| 32 | - | step loginStep | |
| 33 | - | method string // "apikey" | "oauth" | |
| 34 | - | provider string // "anthropic" | "openai" | "openai-codex" | "kimi" | "google" | |
| 35 | - | message string | |
| 36 | - | success bool | |
| 37 | - | url string | |
| 38 | - | cursor int | |
| 39 | - | codeEd *tui.Editor | |
| 34 | + | step loginStep | |
| 35 | + | method string // "apikey" | "oauth" | |
| 36 | + | provider string // "anthropic" | "openai" | "openai-codex" | "kimi" | "google" | |
| 37 | + | message string | |
| 38 | + | success bool | |
| 39 | + | url string | |
| 40 | + | cursor int | |
| 41 | + | codeEd *tui.Editor | |
| 42 | + | infoTitle string | |
| 43 | + | infoLines []string | |
| 40 | 44 | ||
| 41 | 45 | // status is a snapshot of the current login state for each | |
| 42 | 46 | // provider, captured when Open() runs. Rendered above the | |
| @@ -68,6 +72,8 @@ func (d *loginDialog) Open(zotHome string) { | |||
| 68 | 72 | d.success = false | |
| 69 | 73 | d.url = "" | |
| 70 | 74 | d.cursor = 0 | |
| 75 | + | d.infoTitle = "" | |
| 76 | + | d.infoLines = nil | |
| 71 | 77 | d.status = map[string]string{} | |
| 72 | 78 | for _, p := range providersForMethod("apikey") { | |
| 73 | 79 | d.status[p] = "" | |
| @@ -199,6 +205,18 @@ func (d *loginDialog) Render(th tui.Theme, width int) []string { | |||
| 199 | 205 | lines = append(lines, "") | |
| 200 | 206 | lines = append(lines, th.FG256(th.Muted, "enter submits - esc cancels")) | |
| 201 | 207 | lines = append(lines, frameRule(th, width)) | |
| 208 | + | case loginStepInfo: | |
| 209 | + | title := d.infoTitle | |
| 210 | + | if title == "" { | |
| 211 | + | title = "login - setup" | |
| 212 | + | } | |
| 213 | + | lines = append(lines, frameHeader(th, title, width)) | |
| 214 | + | for _, l := range d.infoLines { | |
| 215 | + | lines = append(lines, l) | |
| 216 | + | } | |
| 217 | + | lines = append(lines, "") | |
| 218 | + | lines = append(lines, th.FG256(th.Muted, "press any key to close")) | |
| 219 | + | lines = append(lines, frameRule(th, width)) | |
| 202 | 220 | case loginStepDone: | |
| 203 | 221 | title := "login - failed" | |
| 204 | 222 | body := th.FG256(th.Error, d.message) | |
| @@ -221,10 +239,16 @@ func (d *loginDialog) Render(th tui.Theme, width int) []string { | |||
| 221 | 239 | // consumer Gemini Advanced login does not, and DeepSeek has no | |
| 222 | 240 | // subscription product at all). | |
| 223 | 241 | func providersForMethod(method string) []string { | |
| 242 | + | var providers []string | |
| 224 | 243 | if method == "oauth" { | |
| 225 | - | return []string{"anthropic", "openai-codex", "kimi", "github-copilot"} | |
| 244 | + | providers = []string{"anthropic", "openai-codex", "kimi", "github-copilot"} | |
| 245 | + | } else { | |
| 246 | + | providers = auth.APIKeyProviders() | |
| 226 | 247 | } | |
| 227 | - | return auth.APIKeyProviders() | |
| 248 | + | sort.Slice(providers, func(a, b int) bool { | |
| 249 | + | return providerLabel(providers[a]) < providerLabel(providers[b]) | |
| 250 | + | }) | |
| 251 | + | return providers | |
| 228 | 252 | } | |
| 229 | 253 | ||
| 230 | 254 | // providerLabel returns the user-facing label for a provider id. | |
| @@ -347,6 +371,9 @@ func (d *loginDialog) HandleKey(k tui.Key) loginDialogAction { | |||
| 347 | 371 | return d.handleWaitingKey(k) | |
| 348 | 372 | case loginStepPasteCode: | |
| 349 | 373 | return d.handlePasteCodeKey(k) | |
| 374 | + | case loginStepInfo: | |
| 375 | + | d.Close() | |
| 376 | + | return loginDialogAction{Close: true} | |
| 350 | 377 | case loginStepDone: | |
| 351 | 378 | d.Close() | |
| 352 | 379 | return loginDialogAction{Close: true} | |
| @@ -430,6 +457,17 @@ func (d *loginDialog) ShowWaiting(url string) { | |||
| 430 | 457 | d.url = url | |
| 431 | 458 | } | |
| 432 | 459 | ||
| 460 | + | // ShowInfo transitions to an informational setup dialog. | |
| 461 | + | // No-op if the user has already dismissed the dialog. | |
| 462 | + | func (d *loginDialog) ShowInfo(title string, lines []string) { | |
| 463 | + | if d.step == loginStepClosed { | |
| 464 | + | return | |
| 465 | + | } | |
| 466 | + | d.step = loginStepInfo | |
| 467 | + | d.infoTitle = title | |
| 468 | + | d.infoLines = lines | |
| 469 | + | } | |
| 470 | + | ||
| 433 | 471 | // ShowResult transitions to the done state with the given outcome. | |
| 434 | 472 | // No-op if the user has already dismissed the dialog. | |
| 435 | 473 | func (d *loginDialog) ShowResult(success bool, message string) { | |
| @@ -3,6 +3,7 @@ package modes | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "fmt" | 4 | "fmt" | ||
| 5 | "path/filepath" | 5 | "path/filepath" | ||
| 6 | + | "sort" | |||
| 6 | 7 | ||||
| 7 | "github.com/patriceckhart/zot/packages/provider" | 8 | "github.com/patriceckhart/zot/packages/provider" | ||
| 8 | "github.com/patriceckhart/zot/packages/provider/auth" | 9 | "github.com/patriceckhart/zot/packages/provider/auth" | ||
| @@ -21,6 +22,7 @@ const ( | |||||
| 21 | loginStepProvider // pick anthropic vs openai vs kimi | 22 | loginStepProvider // pick anthropic vs openai vs kimi | ||
| 22 | loginStepWaiting // browser open, waiting for callback | 23 | loginStepWaiting // browser open, waiting for callback | ||
| 23 | loginStepPasteCode // user pastes the auth code here | 24 | loginStepPasteCode // user pastes the auth code here | ||
| 25 | + | loginStepInfo // informational setup guidance | |||
| 24 | loginStepDone // success or error, waiting for key to dismiss | 26 | loginStepDone // success or error, waiting for key to dismiss | ||
| 25 | ) | 27 | ) | ||
| 26 | 28 | ||||
| @@ -29,14 +31,16 @@ const loginProviderPageSize = 8 | |||||
| 29 | // loginDialog is a tiny inline dialog rendered above the editor while | 31 | // loginDialog is a tiny inline dialog rendered above the editor while | ||
| 30 | // the user picks their login method and provider. | 32 | // the user picks their login method and provider. | ||
| 31 | type loginDialog struct { | 33 | type loginDialog struct { | ||
| 32 | - | step loginStep | 34 | + | step loginStep |
| 33 | - | method string // "apikey" | "oauth" | 35 | + | method string // "apikey" | "oauth" |
| 34 | - | provider string // "anthropic" | "openai" | "openai-codex" | "kimi" | "google" | 36 | + | provider string // "anthropic" | "openai" | "openai-codex" | "kimi" | "google" |
| 35 | - | message string | 37 | + | message string |
| 36 | - | success bool | 38 | + | success bool |
| 37 | - | url string | 39 | + | url string |
| 38 | - | cursor int | 40 | + | cursor int |
| 39 | - | codeEd *tui.Editor | 41 | + | codeEd *tui.Editor |
| 42 | + | infoTitle string | |||
| 43 | + | infoLines []string | |||
| 40 | 44 | ||||
| 41 | // status is a snapshot of the current login state for each | 45 | // status is a snapshot of the current login state for each | ||
| 42 | // provider, captured when Open() runs. Rendered above the | 46 | // provider, captured when Open() runs. Rendered above the | ||
| @@ -68,6 +72,8 @@ func (d *loginDialog) Open(zotHome string) { | |||||
| 68 | d.success = false | 72 | d.success = false | ||
| 69 | d.url = "" | 73 | d.url = "" | ||
| 70 | d.cursor = 0 | 74 | d.cursor = 0 | ||
| 75 | + | d.infoTitle = "" | |||
| 76 | + | d.infoLines = nil | |||
| 71 | d.status = map[string]string{} | 77 | d.status = map[string]string{} | ||
| 72 | for _, p := range providersForMethod("apikey") { | 78 | for _, p := range providersForMethod("apikey") { | ||
| 73 | d.status[p] = "" | 79 | d.status[p] = "" | ||
| @@ -199,6 +205,18 @@ func (d *loginDialog) Render(th tui.Theme, width int) []string { | |||||
| 199 | lines = append(lines, "") | 205 | lines = append(lines, "") | ||
| 200 | lines = append(lines, th.FG256(th.Muted, "enter submits - esc cancels")) | 206 | lines = append(lines, th.FG256(th.Muted, "enter submits - esc cancels")) | ||
| 201 | lines = append(lines, frameRule(th, width)) | 207 | lines = append(lines, frameRule(th, width)) | ||
| 208 | + | case loginStepInfo: | |||
| 209 | + | title := d.infoTitle | |||
| 210 | + | if title == "" { | |||
| 211 | + | title = "login - setup" | |||
| 212 | + | } | |||
| 213 | + | lines = append(lines, frameHeader(th, title, width)) | |||
| 214 | + | for _, l := range d.infoLines { | |||
| 215 | + | lines = append(lines, l) | |||
| 216 | + | } | |||
| 217 | + | lines = append(lines, "") | |||
| 218 | + | lines = append(lines, th.FG256(th.Muted, "press any key to close")) | |||
| 219 | + | lines = append(lines, frameRule(th, width)) | |||
| 202 | case loginStepDone: | 220 | case loginStepDone: | ||
| 203 | title := "login - failed" | 221 | title := "login - failed" | ||
| 204 | body := th.FG256(th.Error, d.message) | 222 | body := th.FG256(th.Error, d.message) | ||
| @@ -221,10 +239,16 @@ func (d *loginDialog) Render(th tui.Theme, width int) []string { | |||||
| 221 | // consumer Gemini Advanced login does not, and DeepSeek has no | 239 | // consumer Gemini Advanced login does not, and DeepSeek has no | ||
| 222 | // subscription product at all). | 240 | // subscription product at all). | ||
| 223 | func providersForMethod(method string) []string { | 241 | func providersForMethod(method string) []string { | ||
| 242 | + | var providers []string | |||
| 224 | if method == "oauth" { | 243 | if method == "oauth" { | ||
| 225 | - | return []string{"anthropic", "openai-codex", "kimi", "github-copilot"} | 244 | + | providers = []string{"anthropic", "openai-codex", "kimi", "github-copilot"} |
| 245 | + | } else { | |||
| 246 | + | providers = auth.APIKeyProviders() | |||
| 226 | } | 247 | } | ||
| 227 | - | return auth.APIKeyProviders() | 248 | + | sort.Slice(providers, func(a, b int) bool { |
| 249 | + | return providerLabel(providers[a]) < providerLabel(providers[b]) | |||
| 250 | + | }) | |||
| 251 | + | return providers | |||
| 228 | } | 252 | } | ||
| 229 | 253 | ||||
| 230 | // providerLabel returns the user-facing label for a provider id. | 254 | // providerLabel returns the user-facing label for a provider id. | ||
| @@ -347,6 +371,9 @@ func (d *loginDialog) HandleKey(k tui.Key) loginDialogAction { | |||||
| 347 | return d.handleWaitingKey(k) | 371 | return d.handleWaitingKey(k) | ||
| 348 | case loginStepPasteCode: | 372 | case loginStepPasteCode: | ||
| 349 | return d.handlePasteCodeKey(k) | 373 | return d.handlePasteCodeKey(k) | ||
| 374 | + | case loginStepInfo: | |||
| 375 | + | d.Close() | |||
| 376 | + | return loginDialogAction{Close: true} | |||
| 350 | case loginStepDone: | 377 | case loginStepDone: | ||
| 351 | d.Close() | 378 | d.Close() | ||
| 352 | return loginDialogAction{Close: true} | 379 | return loginDialogAction{Close: true} | ||
| @@ -430,6 +457,17 @@ func (d *loginDialog) ShowWaiting(url string) { | |||||
| 430 | d.url = url | 457 | d.url = url | ||
| 431 | } | 458 | } | ||
| 432 | 459 | ||||
| 460 | + | // ShowInfo transitions to an informational setup dialog. | |||
| 461 | + | // No-op if the user has already dismissed the dialog. | |||
| 462 | + | func (d *loginDialog) ShowInfo(title string, lines []string) { | |||
| 463 | + | if d.step == loginStepClosed { | |||
| 464 | + | return | |||
| 465 | + | } | |||
| 466 | + | d.step = loginStepInfo | |||
| 467 | + | d.infoTitle = title | |||
| 468 | + | d.infoLines = lines | |||
| 469 | + | } | |||
| 470 | + | ||||
| 433 | // ShowResult transitions to the done state with the given outcome. | 471 | // ShowResult transitions to the done state with the given outcome. | ||
| 434 | // No-op if the user has already dismissed the dialog. | 472 | // No-op if the user has already dismissed the dialog. | ||
| 435 | func (d *loginDialog) ShowResult(success bool, message string) { | 473 | func (d *loginDialog) ShowResult(success bool, message string) { | ||
M packages/provider/amazon_bedrock.go +36 -6
| @@ -450,6 +450,9 @@ func (c *bedrockClient) runStream(ctx context.Context, resp *http.Response, req | |||
| 450 | 450 | return | |
| 451 | 451 | } | |
| 452 | 452 | eventType := evt.headerString(":event-type") | |
| 453 | + | if eventType == "" { | |
| 454 | + | eventType = bedrockEventTypeFromPayload(evt.payload) | |
| 455 | + | } | |
| 453 | 456 | messageType := evt.headerString(":message-type") | |
| 454 | 457 | if messageType == "exception" { | |
| 455 | 458 | out <- EventDone{Stop: StopError, Err: fmt.Errorf("bedrock exception (%s): %s", evt.headerString(":exception-type"), string(evt.payload)), Message: finalMsg} | |
| @@ -468,7 +471,7 @@ func (c *bedrockClient) runStream(ctx context.Context, resp *http.Response, req | |||
| 468 | 471 | } `json:"toolUse"` | |
| 469 | 472 | } `json:"start"` | |
| 470 | 473 | } | |
| 471 | - | if err := json.Unmarshal(evt.payload, &d); err != nil { | |
| 474 | + | if err := unmarshalBedrockEventPayload(evt.payload, "contentBlockStart", &d); err != nil { | |
| 472 | 475 | continue | |
| 473 | 476 | } | |
| 474 | 477 | st := &bedrockBlockState{} | |
| @@ -489,12 +492,13 @@ func (c *bedrockClient) runStream(ctx context.Context, resp *http.Response, req | |||
| 489 | 492 | } `json:"toolUse"` | |
| 490 | 493 | } `json:"delta"` | |
| 491 | 494 | } | |
| 492 | - | if err := json.Unmarshal(evt.payload, &d); err != nil { | |
| 495 | + | if err := unmarshalBedrockEventPayload(evt.payload, "contentBlockDelta", &d); err != nil { | |
| 493 | 496 | continue | |
| 494 | 497 | } | |
| 495 | 498 | st := contentBlocks[d.ContentBlockIndex] | |
| 496 | 499 | if st == nil { | |
| 497 | - | continue | |
| 500 | + | st = &bedrockBlockState{} | |
| 501 | + | contentBlocks[d.ContentBlockIndex] = st | |
| 498 | 502 | } | |
| 499 | 503 | if d.Delta.Text != "" { | |
| 500 | 504 | st.text.WriteString(d.Delta.Text) | |
| @@ -508,7 +512,7 @@ func (c *bedrockClient) runStream(ctx context.Context, resp *http.Response, req | |||
| 508 | 512 | var d struct { | |
| 509 | 513 | ContentBlockIndex int `json:"contentBlockIndex"` | |
| 510 | 514 | } | |
| 511 | - | if err := json.Unmarshal(evt.payload, &d); err != nil { | |
| 515 | + | if err := unmarshalBedrockEventPayload(evt.payload, "contentBlockStop", &d); err != nil { | |
| 512 | 516 | continue | |
| 513 | 517 | } | |
| 514 | 518 | st := contentBlocks[d.ContentBlockIndex] | |
| @@ -531,7 +535,7 @@ func (c *bedrockClient) runStream(ctx context.Context, resp *http.Response, req | |||
| 531 | 535 | var d struct { | |
| 532 | 536 | StopReason string `json:"stopReason"` | |
| 533 | 537 | } | |
| 534 | - | _ = json.Unmarshal(evt.payload, &d) | |
| 538 | + | _ = unmarshalBedrockEventPayload(evt.payload, "messageStop", &d) | |
| 535 | 539 | switch d.StopReason { | |
| 536 | 540 | case "tool_use": | |
| 537 | 541 | stop = StopToolUse | |
| @@ -551,7 +555,7 @@ func (c *bedrockClient) runStream(ctx context.Context, resp *http.Response, req | |||
| 551 | 555 | CacheWriteInputTokens int `json:"cacheWriteInputTokens"` | |
| 552 | 556 | } `json:"usage"` | |
| 553 | 557 | } | |
| 554 | - | if err := json.Unmarshal(evt.payload, &d); err == nil { | |
| 558 | + | if err := unmarshalBedrockEventPayload(evt.payload, "metadata", &d); err == nil { | |
| 555 | 559 | usage.InputTokens = d.Usage.InputTokens | |
| 556 | 560 | usage.OutputTokens = d.Usage.OutputTokens | |
| 557 | 561 | usage.CacheReadTokens = d.Usage.CacheReadInputTokens | |
| @@ -574,6 +578,32 @@ type bedrockBlockState struct { | |||
| 574 | 578 | text strings.Builder | |
| 575 | 579 | } | |
| 576 | 580 | ||
| 581 | + | func bedrockEventTypeFromPayload(payload []byte) string { | |
| 582 | + | var outer map[string]json.RawMessage | |
| 583 | + | if err := json.Unmarshal(payload, &outer); err != nil { | |
| 584 | + | return "" | |
| 585 | + | } | |
| 586 | + | for _, name := range []string{"messageStart", "contentBlockStart", "contentBlockDelta", "contentBlockStop", "messageStop", "metadata"} { | |
| 587 | + | if _, ok := outer[name]; ok { | |
| 588 | + | return name | |
| 589 | + | } | |
| 590 | + | } | |
| 591 | + | return "" | |
| 592 | + | } | |
| 593 | + | ||
| 594 | + | func unmarshalBedrockEventPayload(payload []byte, eventType string, dst any) error { | |
| 595 | + | var outer map[string]json.RawMessage | |
| 596 | + | if err := json.Unmarshal(payload, &outer); err == nil { | |
| 597 | + | if wrapped, ok := outer[eventType]; ok { | |
| 598 | + | return json.Unmarshal(wrapped, dst) | |
| 599 | + | } | |
| 600 | + | } | |
| 601 | + | if err := json.Unmarshal(payload, dst); err != nil { | |
| 602 | + | return fmt.Errorf("bedrock: parse %s payload: %w", eventType, err) | |
| 603 | + | } | |
| 604 | + | return nil | |
| 605 | + | } | |
| 606 | + | ||
| 577 | 607 | // ---- event-stream binary framing parser ---- | |
| 578 | 608 | ||
| 579 | 609 | type eventStreamMessage struct { | |
| @@ -450,6 +450,9 @@ func (c *bedrockClient) runStream(ctx context.Context, resp *http.Response, req | |||||
| 450 | return | 450 | return | ||
| 451 | } | 451 | } | ||
| 452 | eventType := evt.headerString(":event-type") | 452 | eventType := evt.headerString(":event-type") | ||
| 453 | + | if eventType == "" { | |||
| 454 | + | eventType = bedrockEventTypeFromPayload(evt.payload) | |||
| 455 | + | } | |||
| 453 | messageType := evt.headerString(":message-type") | 456 | messageType := evt.headerString(":message-type") | ||
| 454 | if messageType == "exception" { | 457 | if messageType == "exception" { | ||
| 455 | out <- EventDone{Stop: StopError, Err: fmt.Errorf("bedrock exception (%s): %s", evt.headerString(":exception-type"), string(evt.payload)), Message: finalMsg} | 458 | out <- EventDone{Stop: StopError, Err: fmt.Errorf("bedrock exception (%s): %s", evt.headerString(":exception-type"), string(evt.payload)), Message: finalMsg} | ||
| @@ -468,7 +471,7 @@ func (c *bedrockClient) runStream(ctx context.Context, resp *http.Response, req | |||||
| 468 | } `json:"toolUse"` | 471 | } `json:"toolUse"` | ||
| 469 | } `json:"start"` | 472 | } `json:"start"` | ||
| 470 | } | 473 | } | ||
| 471 | - | if err := json.Unmarshal(evt.payload, &d); err != nil { | 474 | + | if err := unmarshalBedrockEventPayload(evt.payload, "contentBlockStart", &d); err != nil { |
| 472 | continue | 475 | continue | ||
| 473 | } | 476 | } | ||
| 474 | st := &bedrockBlockState{} | 477 | st := &bedrockBlockState{} | ||
| @@ -489,12 +492,13 @@ func (c *bedrockClient) runStream(ctx context.Context, resp *http.Response, req | |||||
| 489 | } `json:"toolUse"` | 492 | } `json:"toolUse"` | ||
| 490 | } `json:"delta"` | 493 | } `json:"delta"` | ||
| 491 | } | 494 | } | ||
| 492 | - | if err := json.Unmarshal(evt.payload, &d); err != nil { | 495 | + | if err := unmarshalBedrockEventPayload(evt.payload, "contentBlockDelta", &d); err != nil { |
| 493 | continue | 496 | continue | ||
| 494 | } | 497 | } | ||
| 495 | st := contentBlocks[d.ContentBlockIndex] | 498 | st := contentBlocks[d.ContentBlockIndex] | ||
| 496 | if st == nil { | 499 | if st == nil { | ||
| 497 | - | continue | 500 | + | st = &bedrockBlockState{} |
| 501 | + | contentBlocks[d.ContentBlockIndex] = st | |||
| 498 | } | 502 | } | ||
| 499 | if d.Delta.Text != "" { | 503 | if d.Delta.Text != "" { | ||
| 500 | st.text.WriteString(d.Delta.Text) | 504 | st.text.WriteString(d.Delta.Text) | ||
| @@ -508,7 +512,7 @@ func (c *bedrockClient) runStream(ctx context.Context, resp *http.Response, req | |||||
| 508 | var d struct { | 512 | var d struct { | ||
| 509 | ContentBlockIndex int `json:"contentBlockIndex"` | 513 | ContentBlockIndex int `json:"contentBlockIndex"` | ||
| 510 | } | 514 | } | ||
| 511 | - | if err := json.Unmarshal(evt.payload, &d); err != nil { | 515 | + | if err := unmarshalBedrockEventPayload(evt.payload, "contentBlockStop", &d); err != nil { |
| 512 | continue | 516 | continue | ||
| 513 | } | 517 | } | ||
| 514 | st := contentBlocks[d.ContentBlockIndex] | 518 | st := contentBlocks[d.ContentBlockIndex] | ||
| @@ -531,7 +535,7 @@ func (c *bedrockClient) runStream(ctx context.Context, resp *http.Response, req | |||||
| 531 | var d struct { | 535 | var d struct { | ||
| 532 | StopReason string `json:"stopReason"` | 536 | StopReason string `json:"stopReason"` | ||
| 533 | } | 537 | } | ||
| 534 | - | _ = json.Unmarshal(evt.payload, &d) | 538 | + | _ = unmarshalBedrockEventPayload(evt.payload, "messageStop", &d) |
| 535 | switch d.StopReason { | 539 | switch d.StopReason { | ||
| 536 | case "tool_use": | 540 | case "tool_use": | ||
| 537 | stop = StopToolUse | 541 | stop = StopToolUse | ||
| @@ -551,7 +555,7 @@ func (c *bedrockClient) runStream(ctx context.Context, resp *http.Response, req | |||||
| 551 | CacheWriteInputTokens int `json:"cacheWriteInputTokens"` | 555 | CacheWriteInputTokens int `json:"cacheWriteInputTokens"` | ||
| 552 | } `json:"usage"` | 556 | } `json:"usage"` | ||
| 553 | } | 557 | } | ||
| 554 | - | if err := json.Unmarshal(evt.payload, &d); err == nil { | 558 | + | if err := unmarshalBedrockEventPayload(evt.payload, "metadata", &d); err == nil { |
| 555 | usage.InputTokens = d.Usage.InputTokens | 559 | usage.InputTokens = d.Usage.InputTokens | ||
| 556 | usage.OutputTokens = d.Usage.OutputTokens | 560 | usage.OutputTokens = d.Usage.OutputTokens | ||
| 557 | usage.CacheReadTokens = d.Usage.CacheReadInputTokens | 561 | usage.CacheReadTokens = d.Usage.CacheReadInputTokens | ||
| @@ -574,6 +578,32 @@ type bedrockBlockState struct { | |||||
| 574 | text strings.Builder | 578 | text strings.Builder | ||
| 575 | } | 579 | } | ||
| 576 | 580 | ||||
| 581 | + | func bedrockEventTypeFromPayload(payload []byte) string { | |||
| 582 | + | var outer map[string]json.RawMessage | |||
| 583 | + | if err := json.Unmarshal(payload, &outer); err != nil { | |||
| 584 | + | return "" | |||
| 585 | + | } | |||
| 586 | + | for _, name := range []string{"messageStart", "contentBlockStart", "contentBlockDelta", "contentBlockStop", "messageStop", "metadata"} { | |||
| 587 | + | if _, ok := outer[name]; ok { | |||
| 588 | + | return name | |||
| 589 | + | } | |||
| 590 | + | } | |||
| 591 | + | return "" | |||
| 592 | + | } | |||
| 593 | + | ||||
| 594 | + | func unmarshalBedrockEventPayload(payload []byte, eventType string, dst any) error { | |||
| 595 | + | var outer map[string]json.RawMessage | |||
| 596 | + | if err := json.Unmarshal(payload, &outer); err == nil { | |||
| 597 | + | if wrapped, ok := outer[eventType]; ok { | |||
| 598 | + | return json.Unmarshal(wrapped, dst) | |||
| 599 | + | } | |||
| 600 | + | } | |||
| 601 | + | if err := json.Unmarshal(payload, dst); err != nil { | |||
| 602 | + | return fmt.Errorf("bedrock: parse %s payload: %w", eventType, err) | |||
| 603 | + | } | |||
| 604 | + | return nil | |||
| 605 | + | } | |||
| 606 | + | ||||
| 577 | // ---- event-stream binary framing parser ---- | 607 | // ---- event-stream binary framing parser ---- | ||
| 578 | 608 | ||||
| 579 | type eventStreamMessage struct { | 609 | type eventStreamMessage struct { | ||
M packages/provider/amazon_bedrock_test.go +27 -0
| @@ -82,6 +82,33 @@ func TestReadAWSCredentialsFile(t *testing.T) { | |||
| 82 | 82 | } | |
| 83 | 83 | } | |
| 84 | 84 | ||
| 85 | + | func TestBedrockEventPayloadHelpers(t *testing.T) { | |
| 86 | + | wrapped := []byte(`{"contentBlockDelta":{"contentBlockIndex":0,"delta":{"text":"Hello"}}}`) | |
| 87 | + | if got := bedrockEventTypeFromPayload(wrapped); got != "contentBlockDelta" { | |
| 88 | + | t.Fatalf("event type = %q, want contentBlockDelta", got) | |
| 89 | + | } | |
| 90 | + | var delta struct { | |
| 91 | + | ContentBlockIndex int `json:"contentBlockIndex"` | |
| 92 | + | Delta struct { | |
| 93 | + | Text string `json:"text"` | |
| 94 | + | } `json:"delta"` | |
| 95 | + | } | |
| 96 | + | if err := unmarshalBedrockEventPayload(wrapped, "contentBlockDelta", &delta); err != nil { | |
| 97 | + | t.Fatal(err) | |
| 98 | + | } | |
| 99 | + | if delta.ContentBlockIndex != 0 || delta.Delta.Text != "Hello" { | |
| 100 | + | t.Fatalf("unexpected wrapped delta: %+v", delta) | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | direct := []byte(`{"contentBlockIndex":1,"delta":{"text":"world"}}`) | |
| 104 | + | if err := unmarshalBedrockEventPayload(direct, "contentBlockDelta", &delta); err != nil { | |
| 105 | + | t.Fatal(err) | |
| 106 | + | } | |
| 107 | + | if delta.ContentBlockIndex != 1 || delta.Delta.Text != "world" { | |
| 108 | + | t.Fatalf("unexpected direct delta: %+v", delta) | |
| 109 | + | } | |
| 110 | + | } | |
| 111 | + | ||
| 85 | 112 | func TestResolveBedrockInferenceProfileID(t *testing.T) { | |
| 86 | 113 | cases := []struct { | |
| 87 | 114 | model string | |
| @@ -82,6 +82,33 @@ func TestReadAWSCredentialsFile(t *testing.T) { | |||||
| 82 | } | 82 | } | ||
| 83 | } | 83 | } | ||
| 84 | 84 | ||||
| 85 | + | func TestBedrockEventPayloadHelpers(t *testing.T) { | |||
| 86 | + | wrapped := []byte(`{"contentBlockDelta":{"contentBlockIndex":0,"delta":{"text":"Hello"}}}`) | |||
| 87 | + | if got := bedrockEventTypeFromPayload(wrapped); got != "contentBlockDelta" { | |||
| 88 | + | t.Fatalf("event type = %q, want contentBlockDelta", got) | |||
| 89 | + | } | |||
| 90 | + | var delta struct { | |||
| 91 | + | ContentBlockIndex int `json:"contentBlockIndex"` | |||
| 92 | + | Delta struct { | |||
| 93 | + | Text string `json:"text"` | |||
| 94 | + | } `json:"delta"` | |||
| 95 | + | } | |||
| 96 | + | if err := unmarshalBedrockEventPayload(wrapped, "contentBlockDelta", &delta); err != nil { | |||
| 97 | + | t.Fatal(err) | |||
| 98 | + | } | |||
| 99 | + | if delta.ContentBlockIndex != 0 || delta.Delta.Text != "Hello" { | |||
| 100 | + | t.Fatalf("unexpected wrapped delta: %+v", delta) | |||
| 101 | + | } | |||
| 102 | + | ||||
| 103 | + | direct := []byte(`{"contentBlockIndex":1,"delta":{"text":"world"}}`) | |||
| 104 | + | if err := unmarshalBedrockEventPayload(direct, "contentBlockDelta", &delta); err != nil { | |||
| 105 | + | t.Fatal(err) | |||
| 106 | + | } | |||
| 107 | + | if delta.ContentBlockIndex != 1 || delta.Delta.Text != "world" { | |||
| 108 | + | t.Fatalf("unexpected direct delta: %+v", delta) | |||
| 109 | + | } | |||
| 110 | + | } | |||
| 111 | + | ||||
| 85 | func TestResolveBedrockInferenceProfileID(t *testing.T) { | 112 | func TestResolveBedrockInferenceProfileID(t *testing.T) { | ||
| 86 | cases := []struct { | 113 | cases := []struct { | ||
| 87 | model string | 114 | model string | ||