detached 7a7bf0b

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) {
34053405 }
34063406 }
34073407
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+
34083476 func (i *Interactive) startAPIKeyFlow(provider string) {
3477+ if title, lines, ok := providerSetupInfo(provider); ok {
3478+ i.dialog.ShowInfo(title, lines)
3479+ return
3480+ }
34093481 if provider == "kimi" && i.cfg.SetKimiCLIFallbackDisabled != nil {
34103482 _ = i.cfg.SetKimiCLIFallbackDisabled(false)
34113483 }
@@ -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
33 import (
44 "fmt"
55 "path/filepath"
6+ "sort"
67
78 "github.com/patriceckhart/zot/packages/provider"
89 "github.com/patriceckhart/zot/packages/provider/auth"
@@ -21,6 +22,7 @@ const (
2122 loginStepProvider // pick anthropic vs openai vs kimi
2223 loginStepWaiting // browser open, waiting for callback
2324 loginStepPasteCode // user pastes the auth code here
25+ loginStepInfo // informational setup guidance
2426 loginStepDone // success or error, waiting for key to dismiss
2527 )
2628
@@ -29,14 +31,16 @@ const loginProviderPageSize = 8
2931 // loginDialog is a tiny inline dialog rendered above the editor while
3032 // the user picks their login method and provider.
3133 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
4044
4145 // status is a snapshot of the current login state for each
4246 // provider, captured when Open() runs. Rendered above the
@@ -68,6 +72,8 @@ func (d *loginDialog) Open(zotHome string) {
6872 d.success = false
6973 d.url = ""
7074 d.cursor = 0
75+ d.infoTitle = ""
76+ d.infoLines = nil
7177 d.status = map[string]string{}
7278 for _, p := range providersForMethod("apikey") {
7379 d.status[p] = ""
@@ -199,6 +205,18 @@ func (d *loginDialog) Render(th tui.Theme, width int) []string {
199205 lines = append(lines, "")
200206 lines = append(lines, th.FG256(th.Muted, "enter submits - esc cancels"))
201207 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))
202220 case loginStepDone:
203221 title := "login - failed"
204222 body := th.FG256(th.Error, d.message)
@@ -221,10 +239,16 @@ func (d *loginDialog) Render(th tui.Theme, width int) []string {
221239 // consumer Gemini Advanced login does not, and DeepSeek has no
222240 // subscription product at all).
223241 func providersForMethod(method string) []string {
242+ var providers []string
224243 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()
226247 }
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
228252 }
229253
230254 // providerLabel returns the user-facing label for a provider id.
@@ -347,6 +371,9 @@ func (d *loginDialog) HandleKey(k tui.Key) loginDialogAction {
347371 return d.handleWaitingKey(k)
348372 case loginStepPasteCode:
349373 return d.handlePasteCodeKey(k)
374+ case loginStepInfo:
375+ d.Close()
376+ return loginDialogAction{Close: true}
350377 case loginStepDone:
351378 d.Close()
352379 return loginDialogAction{Close: true}
@@ -430,6 +457,17 @@ func (d *loginDialog) ShowWaiting(url string) {
430457 d.url = url
431458 }
432459
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+
433471 // ShowResult transitions to the done state with the given outcome.
434472 // No-op if the user has already dismissed the dialog.
435473 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 kimi22 loginStepProvider // pick anthropic vs openai vs kimi
22 loginStepWaiting // browser open, waiting for callback23 loginStepWaiting // browser open, waiting for callback
23 loginStepPasteCode // user pastes the auth code here24 loginStepPasteCode // user pastes the auth code here
25+ loginStepInfo // informational setup guidance
24 loginStepDone // success or error, waiting for key to dismiss26 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 while31 // 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 loginStep34+ 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 string37+ message string
36- success bool38+ success bool
37- url string39+ url string
38- cursor int40+ cursor int
39- codeEd *tui.Editor41+ codeEd *tui.Editor
42+ infoTitle string
43+ infoLines []string
40 44
41 // status is a snapshot of the current login state for each45 // status is a snapshot of the current login state for each
42 // provider, captured when Open() runs. Rendered above the46 // provider, captured when Open() runs. Rendered above the
@@ -68,6 +72,8 @@ func (d *loginDialog) Open(zotHome string) {
68 d.success = false72 d.success = false
69 d.url = ""73 d.url = ""
70 d.cursor = 074 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 no239 // 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 = url457 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
450450 return
451451 }
452452 eventType := evt.headerString(":event-type")
453+ if eventType == "" {
454+ eventType = bedrockEventTypeFromPayload(evt.payload)
455+ }
453456 messageType := evt.headerString(":message-type")
454457 if messageType == "exception" {
455458 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
468471 } `json:"toolUse"`
469472 } `json:"start"`
470473 }
471- if err := json.Unmarshal(evt.payload, &d); err != nil {
474+ if err := unmarshalBedrockEventPayload(evt.payload, "contentBlockStart", &d); err != nil {
472475 continue
473476 }
474477 st := &bedrockBlockState{}
@@ -489,12 +492,13 @@ func (c *bedrockClient) runStream(ctx context.Context, resp *http.Response, req
489492 } `json:"toolUse"`
490493 } `json:"delta"`
491494 }
492- if err := json.Unmarshal(evt.payload, &d); err != nil {
495+ if err := unmarshalBedrockEventPayload(evt.payload, "contentBlockDelta", &d); err != nil {
493496 continue
494497 }
495498 st := contentBlocks[d.ContentBlockIndex]
496499 if st == nil {
497- continue
500+ st = &bedrockBlockState{}
501+ contentBlocks[d.ContentBlockIndex] = st
498502 }
499503 if d.Delta.Text != "" {
500504 st.text.WriteString(d.Delta.Text)
@@ -508,7 +512,7 @@ func (c *bedrockClient) runStream(ctx context.Context, resp *http.Response, req
508512 var d struct {
509513 ContentBlockIndex int `json:"contentBlockIndex"`
510514 }
511- if err := json.Unmarshal(evt.payload, &d); err != nil {
515+ if err := unmarshalBedrockEventPayload(evt.payload, "contentBlockStop", &d); err != nil {
512516 continue
513517 }
514518 st := contentBlocks[d.ContentBlockIndex]
@@ -531,7 +535,7 @@ func (c *bedrockClient) runStream(ctx context.Context, resp *http.Response, req
531535 var d struct {
532536 StopReason string `json:"stopReason"`
533537 }
534- _ = json.Unmarshal(evt.payload, &d)
538+ _ = unmarshalBedrockEventPayload(evt.payload, "messageStop", &d)
535539 switch d.StopReason {
536540 case "tool_use":
537541 stop = StopToolUse
@@ -551,7 +555,7 @@ func (c *bedrockClient) runStream(ctx context.Context, resp *http.Response, req
551555 CacheWriteInputTokens int `json:"cacheWriteInputTokens"`
552556 } `json:"usage"`
553557 }
554- if err := json.Unmarshal(evt.payload, &d); err == nil {
558+ if err := unmarshalBedrockEventPayload(evt.payload, "metadata", &d); err == nil {
555559 usage.InputTokens = d.Usage.InputTokens
556560 usage.OutputTokens = d.Usage.OutputTokens
557561 usage.CacheReadTokens = d.Usage.CacheReadInputTokens
@@ -574,6 +578,32 @@ type bedrockBlockState struct {
574578 text strings.Builder
575579 }
576580
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+
577607 // ---- event-stream binary framing parser ----
578608
579609 type eventStreamMessage struct {
@@ -450,6 +450,9 @@ func (c *bedrockClient) runStream(ctx context.Context, resp *http.Response, req
450 return450 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 continue475 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 continue496 continue
494 }497 }
495 st := contentBlocks[d.ContentBlockIndex]498 st := contentBlocks[d.ContentBlockIndex]
496 if st == nil {499 if st == nil {
497- continue500+ 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 continue516 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 = StopToolUse541 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.InputTokens559 usage.InputTokens = d.Usage.InputTokens
556 usage.OutputTokens = d.Usage.OutputTokens560 usage.OutputTokens = d.Usage.OutputTokens
557 usage.CacheReadTokens = d.Usage.CacheReadInputTokens561 usage.CacheReadTokens = d.Usage.CacheReadInputTokens
@@ -574,6 +578,32 @@ type bedrockBlockState struct {
574 text strings.Builder578 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) {
8282 }
8383 }
8484
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+
85112 func TestResolveBedrockInferenceProfileID(t *testing.T) {
86113 cases := []struct {
87114 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 string114 model string

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close