catence 0.2.0-beta.1 → 0.2.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +144 -2
- package/config.example.json +32 -20
- package/dist/contracts/index.d.ts +1 -0
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/progress.d.ts +23 -0
- package/dist/contracts/progress.js +48 -0
- package/dist/contracts/release.d.ts +1 -1
- package/dist/contracts/release.js +1 -1
- package/dist/core/logging.d.ts +16 -0
- package/dist/core/logging.js +68 -0
- package/dist/core/query/repository.d.ts +2 -0
- package/dist/core/query/repository.js +42 -0
- package/dist/core/runtime/catalog.js +17 -7
- package/dist/core/runtime/self-update.d.ts +44 -0
- package/dist/core/runtime/self-update.js +138 -0
- package/dist/elt/application/management.d.ts +2 -0
- package/dist/elt/application/management.js +194 -17
- package/dist/elt/ingestion/importer.d.ts +3 -2
- package/dist/elt/ingestion/importer.js +4 -3
- package/dist/elt/ingestion/providers/intervals.d.ts +3 -1
- package/dist/elt/ingestion/providers/intervals.js +4 -1
- package/dist/elt/ingestion/providers/strava/service.js +7 -1
- package/dist/elt/storage/database.d.ts +7 -2
- package/dist/elt/storage/database.js +81 -4
- package/dist/elt/storage/migrations.js +25 -1
- package/dist/interfaces/cli/commands.js +91 -1
- package/dist/interfaces/mcp/server.js +7 -0
- package/dist/runtime/index.d.ts +4 -1
- package/dist/runtime/index.js +2 -1
- package/docs/deployment.md +338 -0
- package/docs/docker-deployment.md +232 -0
- package/docs/llm-providers.md +467 -0
- package/package.json +5 -1
- package/python/catence/providers/garmin/cli.py +30 -9
- package/python/catence/providers/garmin/progress.py +85 -0
- package/python/catence/providers/garmin/worker.py +132 -5
- package/scripts/discover-opencode-go.mjs +210 -0
- package/scripts/discover-opencode-zen.mjs +218 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# Docker Deployment Quick Start and Common Commands
|
|
2
|
+
|
|
3
|
+
This document mirrors the [local quick start](../README.md#quick-start) and [common operations](../README.md#common-operations) for Catence when deployed via Docker. It assumes you have completed the [deployment steps](deployment.md#path-b--docker-deployment) and have a running `catence-deploy` stack.
|
|
4
|
+
|
|
5
|
+
## Quick Start (Docker)
|
|
6
|
+
|
|
7
|
+
All commands run from the `catence-deploy` directory created by the deploy script.
|
|
8
|
+
|
|
9
|
+
### 1. Initialize an athlete store
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
docker compose -f docker-compose.yml --env-file .env run --rm \
|
|
13
|
+
--entrypoint catence-data console setup --athlete alex --label "Alex"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### 2. Store provider credentials (stdin only, never shell history)
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
# Garmin
|
|
20
|
+
printf %s 'alex@example.com' | docker compose -f docker-compose.yml --env-file .env run --rm -T \
|
|
21
|
+
--entrypoint catence-data console --athlete alex secret set \
|
|
22
|
+
--provider garmin --field email --value-stdin
|
|
23
|
+
printf %s 'your-garmin-password' | docker compose -f docker-compose.yml --env-file .env run --rm -T \
|
|
24
|
+
--entrypoint catence-data console --athlete alex secret set \
|
|
25
|
+
--provider garmin --field password --value-stdin
|
|
26
|
+
|
|
27
|
+
# Intervals.icu
|
|
28
|
+
printf %s 'intervals-api-key' | docker compose -f docker-compose.yml --env-file .env run --rm -T \
|
|
29
|
+
--entrypoint catence-data console --athlete alex secret set \
|
|
30
|
+
--provider intervals --field apiKey --value-stdin
|
|
31
|
+
printf %s '12345' | docker compose -f docker-compose.yml --env-file .env run --rm -T \
|
|
32
|
+
--entrypoint catence-data console --athlete alex secret set \
|
|
33
|
+
--provider intervals --field athleteId --value-stdin
|
|
34
|
+
|
|
35
|
+
# Strava (create an API application at https://www.strava.com/settings/api)
|
|
36
|
+
printf %s 'strava-client-id' | docker compose -f docker-compose.yml --env-file .env run --rm -T \
|
|
37
|
+
--entrypoint catence-data console --athlete alex secret set \
|
|
38
|
+
--provider strava --field clientId --value-stdin
|
|
39
|
+
printf %s 'strava-client-secret' | docker compose -f docker-compose.yml --env-file .env run --rm -T \
|
|
40
|
+
--entrypoint catence-data console --athlete alex secret set \
|
|
41
|
+
--provider strava --field clientSecret --value-stdin
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 3. Sync data and build retrieval context
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
docker compose -f docker-compose.yml --env-file .env exec console \
|
|
48
|
+
catence-data sync --athlete alex --provider all --home /data
|
|
49
|
+
docker compose -f docker-compose.yml --env-file .env exec console \
|
|
50
|
+
catence-data build-retrieval-index --athlete alex --home /data
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 4. Access the Console
|
|
54
|
+
|
|
55
|
+
Open `http://127.0.0.1:8000` (or the `--bind:--port` you configured) and log in with `CATENCE_CONSOLE_USERNAME` and the password whose hash is in `CATENCE_CONSOLE_PASSWORD_HASH`.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Common Operations (Docker)
|
|
60
|
+
|
|
61
|
+
The `sync.sh` helper in `catence-deploy/` wraps the most frequent `catence-data` calls inside the **running** container (so the provider secrets from the data volume are already available).
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
# Incremental sync for one athlete (all providers)
|
|
65
|
+
./sync.sh --athlete alex sync
|
|
66
|
+
|
|
67
|
+
# Sync a specific provider
|
|
68
|
+
./sync.sh --athlete alex sync --provider intervals
|
|
69
|
+
|
|
70
|
+
# Sync from a specific date (Garmin)
|
|
71
|
+
./sync.sh --athlete alex sync --provider garmin --from 2025-07-29
|
|
72
|
+
|
|
73
|
+
# Full backfill from a date
|
|
74
|
+
./sync.sh --athlete alex backfill 2026-01-01
|
|
75
|
+
|
|
76
|
+
# Backfill with refresh (re-fetch existing data)
|
|
77
|
+
./sync.sh --athlete alex backfill 2026-01-01 --refresh
|
|
78
|
+
|
|
79
|
+
# Retry a failed sync run
|
|
80
|
+
./sync.sh --athlete alex retry --run <run-id>
|
|
81
|
+
|
|
82
|
+
# Coverage and error status
|
|
83
|
+
./sync.sh --athlete alex status
|
|
84
|
+
|
|
85
|
+
# List athlete IDs in the catalog
|
|
86
|
+
./sync.sh athletes
|
|
87
|
+
|
|
88
|
+
# Strava OAuth (uses clientId/clientSecret stored for this athlete)
|
|
89
|
+
docker compose -f docker-compose.yml --env-file .env exec console \
|
|
90
|
+
catence-data auth strava --athlete alex --callback --home /data
|
|
91
|
+
docker compose -f docker-compose.yml --env-file .env exec console \
|
|
92
|
+
catence-data disconnect strava --athlete alex --home /data
|
|
93
|
+
|
|
94
|
+
# Strava OAuth via SSH (remote Docker host accessed over SSH/Tailscale)
|
|
95
|
+
## Option 1: SSH Port Forwarding (recommended)
|
|
96
|
+
# On your LOCAL machine, forward the callback port to the remote Docker host:
|
|
97
|
+
ssh -L 8765:127.0.0.1:8765 user@docker-host
|
|
98
|
+
# Then run the standard callback flow on the REMOTE host (uses http://127.0.0.1:8765/strava/callback)
|
|
99
|
+
docker compose -f docker-compose.yml --env-file .env exec console \
|
|
100
|
+
catence-data auth strava --athlete alex --callback --home /data
|
|
101
|
+
|
|
102
|
+
## Option 2: Manual code exchange with http://localhost
|
|
103
|
+
# On the REMOTE host, get the authorization URL:
|
|
104
|
+
docker compose -f docker-compose.yml --env-file .env run --rm \
|
|
105
|
+
--entrypoint catence-data console auth strava \
|
|
106
|
+
--athlete alex --redirect-uri "http://localhost" --home /data
|
|
107
|
+
# Open the printed URL in YOUR LOCAL BROWSER, approve Strava, then copy the 'code' from the redirect URL
|
|
108
|
+
# Exchange the code on the REMOTE host:
|
|
109
|
+
docker compose -f docker-compose.yml --env-file .env run --rm \
|
|
110
|
+
--entrypoint catence-data console auth strava \
|
|
111
|
+
--athlete alex --code "THE_CODE_FROM_URL" --redirect-uri "http://localhost" --home /data
|
|
112
|
+
|
|
113
|
+
# Update the runtime and Console (re-run deploy script on the same channel)
|
|
114
|
+
curl -fsSL https://raw.githubusercontent.com/rifusaki/catence/beta/scripts/deploy-console.sh | bash -s beta
|
|
115
|
+
# or from checkout: ./scripts/deploy-console.sh beta
|
|
116
|
+
|
|
117
|
+
# Choose a non-default catalog location (bind mount instead of named volume)
|
|
118
|
+
# Stop the stack, edit docker-compose.yml to replace the volume with:
|
|
119
|
+
# volumes:
|
|
120
|
+
# - /srv/catence:/data
|
|
121
|
+
# Then restart and run the same commands (--home /data is implied by CATENCE_HOME).
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Connecting to the Docker-based MCP from Outside
|
|
127
|
+
|
|
128
|
+
### The MCP Server Inside the Container
|
|
129
|
+
|
|
130
|
+
By default, the Catence MCP server runs on **loopback only** inside the container at `http://127.0.0.1:8787/mcp`. It is **never exposed** to the host network or the internet — this is intentional for security.
|
|
131
|
+
|
|
132
|
+
The `CATENCE_MCP_BIND` variable controls **two things at once**:
|
|
133
|
+
|
|
134
|
+
1. **Docker port mapping**: which host interface exposes port 8787 to the Docker host.
|
|
135
|
+
2. **Server listen address**: which address the MCP server binds to inside the container.
|
|
136
|
+
|
|
137
|
+
| Value | Docker maps | Server binds | Effect |
|
|
138
|
+
|-------|-------------|--------------|--------|
|
|
139
|
+
| `127.0.0.1` (default) | `127.0.0.1:8787 → container:8787` | `127.0.0.1` | MCP reachable only from the Docker host's loopback — requires SSH tunnel for remote access. |
|
|
140
|
+
| `0.0.0.0` | `0.0.0.0:8787 → container:8787` | `0.0.0.0` | MCP reachable from any network interface — suitable for Tailscale or VPN exposure. |
|
|
141
|
+
|
|
142
|
+
Set it at deploy time with `--mcp-bind`, or by editing `CATENCE_MCP_BIND` in `.env`:
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
# Default (loopback only — SSH tunnel required for remote access)
|
|
146
|
+
./scripts/deploy-console.sh beta
|
|
147
|
+
|
|
148
|
+
# Tailscale / network exposure
|
|
149
|
+
./scripts/deploy-console.sh beta --mcp-bind 0.0.0.0
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
To change the binding after deployment, update `CATENCE_MCP_BIND` in `.env` and restart:
|
|
153
|
+
|
|
154
|
+
```sh
|
|
155
|
+
sed -i 's/CATENCE_MCP_BIND=.*/CATENCE_MCP_BIND=0.0.0.0/' catence-deploy/.env
|
|
156
|
+
docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.env up -d
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### SSH Port Forwarding (Recommended for Remote MCP Clients)
|
|
160
|
+
|
|
161
|
+
From your **local machine**, forward the container's MCP port through the Docker host:
|
|
162
|
+
|
|
163
|
+
```sh
|
|
164
|
+
# Forward local port 8787 → Docker host port 8787 → container loopback 8787
|
|
165
|
+
ssh -L 8787:127.0.0.1:8787 user@docker-host
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Then point your MCP client at `http://127.0.0.1:8787/mcp` on your local machine.
|
|
169
|
+
|
|
170
|
+
### Exposing MCP on Tailscale (Alternative)
|
|
171
|
+
|
|
172
|
+
To expose the MCP server directly on a Tailscale interface (or any network interface) without SSH tunneling:
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
# Deploy with MCP bound to all interfaces (0.0.0.0) — restrict via Tailscale ACLs/firewall
|
|
176
|
+
./scripts/deploy-console.sh beta --mcp-bind 0.0.0.0
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Or set the environment variable before deploying:
|
|
180
|
+
|
|
181
|
+
```sh
|
|
182
|
+
CATENCE_MCP_BIND=0.0.0.0 ./scripts/deploy-console.sh beta
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
After deployment, the MCP server is available at `http://<tailscale-ip>:8787/mcp` from any device on your tailnet.
|
|
186
|
+
|
|
187
|
+
**Security note**: The MCP server has **no authentication** — it trusts the caller's `athleteId` parameter. When exposing on `0.0.0.0`, you **must** restrict access at the network layer (Tailscale ACLs, firewall rules, etc.). The Console UI remains on loopback (`127.0.0.1:8000`) and should still be fronted by Cloudflare Tunnel or a reverse proxy.
|
|
188
|
+
|
|
189
|
+
**Examples: Common MCP Clients**
|
|
190
|
+
|
|
191
|
+
```sh
|
|
192
|
+
# Via SSH tunnel (default loopback deployment)
|
|
193
|
+
# Opencode
|
|
194
|
+
opencode mcp add catence -- http://127.0.0.1:8787/mcp
|
|
195
|
+
|
|
196
|
+
# Codex
|
|
197
|
+
codex mcp add catence -- http://127.0.0.1:8787/mcp
|
|
198
|
+
|
|
199
|
+
# Claude Desktop
|
|
200
|
+
claude mcp add --transport http catence -- http://127.0.0.1:8787/mcp
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
```sh
|
|
204
|
+
# Via Tailscale (when deployed with --mcp-bind 0.0.0.0)
|
|
205
|
+
# Replace <tailscale-ip> with your server's Tailscale IP (from `tailscale ip`)
|
|
206
|
+
# Opencode
|
|
207
|
+
opencode mcp add catence -- http://<tailscale-ip>:8787/mcp
|
|
208
|
+
|
|
209
|
+
# Codex
|
|
210
|
+
codex mcp add catence -- http://<tailscale-ip>:8787/mcp
|
|
211
|
+
|
|
212
|
+
# Claude Desktop
|
|
213
|
+
claude mcp add --transport http catence -- http://<tailscale-ip>:8787/mcp
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Why Not Expose Port 8787 Directly?
|
|
217
|
+
|
|
218
|
+
- The MCP server has **no authentication** — it trusts the caller's `athleteId` parameter.
|
|
219
|
+
- Exposing it would allow any network caller to read all athlete data in the catalog.
|
|
220
|
+
- The Console proxies dashboard requests through its **authenticated** same-origin route.
|
|
221
|
+
- SSH port forwarding keeps the trust boundary at your SSH credentials.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Quick Reference: `docker compose run` vs `docker compose exec`
|
|
226
|
+
|
|
227
|
+
| Command Type | Use For | Example |
|
|
228
|
+
|--------------|---------|---------|
|
|
229
|
+
| `run --rm --entrypoint catence-data console` | **One-off setup/secrets** — needs fresh stdin for `--value-stdin`, no running container required | `secret set`, `setup`, `auth strava --code` |
|
|
230
|
+
| `exec console catence-data` | **Recurring ops** — uses the running container's environment (secrets already in volume) | `sync`, `backfill`, `status`, `build-retrieval-index` |
|
|
231
|
+
|
|
232
|
+
The `sync.sh` helper uses `exec` for this reason.
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
# Console LLM providers
|
|
2
|
+
|
|
3
|
+
Catence Console talks to a model through [LiteLLM](https://docs.litellm.ai/).
|
|
4
|
+
There are exactly three provider shapes: **OpenAI**, **Anthropic**, and a
|
|
5
|
+
generic **OpenAI-compatible** profile. Azure is not a special case in the code:
|
|
6
|
+
it is reached through the OpenAI-compatible profile. OpenCode Go can be reached
|
|
7
|
+
the same way through a local gateway, or directly at its own OpenAI-compatible
|
|
8
|
+
endpoint with native `openai/…` and `anthropic/…` model names; the steps for
|
|
9
|
+
each are documented below.
|
|
10
|
+
|
|
11
|
+
## How configuration is split
|
|
12
|
+
|
|
13
|
+
Catence never stores a credential value in `config.json`. A Console profile
|
|
14
|
+
records only a LiteLLM model name and the *names* of the environment variables
|
|
15
|
+
that hold the key, base URL, and (optionally) API version. The values
|
|
16
|
+
themselves stay in the Console process environment.
|
|
17
|
+
|
|
18
|
+
```jsonc
|
|
19
|
+
// ~/.catence/config.json (or /data/config.json inside the Docker image)
|
|
20
|
+
{
|
|
21
|
+
"console": {
|
|
22
|
+
"defaultProfile": "openai",
|
|
23
|
+
"profiles": {
|
|
24
|
+
"openai": {
|
|
25
|
+
"label": "OpenAI",
|
|
26
|
+
"model": "openai/gpt-5-mini",
|
|
27
|
+
"apiKeyEnv": "OPENAI_API_KEY"
|
|
28
|
+
},
|
|
29
|
+
"anthropic": {
|
|
30
|
+
"label": "Anthropic",
|
|
31
|
+
"model": "anthropic/claude-sonnet-4-5",
|
|
32
|
+
"apiKeyEnv": "ANTHROPIC_API_KEY"
|
|
33
|
+
},
|
|
34
|
+
"openai-compatible": {
|
|
35
|
+
"label": "OpenAI-compatible",
|
|
36
|
+
"model": "openai/your-model-name",
|
|
37
|
+
"apiKeyEnv": "OPENAI_API_KEY",
|
|
38
|
+
"apiBaseEnv": "OPENAI_API_BASE"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The supported profile fields are `label`, `model` (or `models` + `defaultModel`
|
|
46
|
+
for multiple deployments), `defaultReasoningEffort`, `apiKeyEnv`, `apiBaseEnv`,
|
|
47
|
+
and `apiVersionEnv`. Every `*Env` value must be an uppercase environment
|
|
48
|
+
variable name; a raw secret there is rejected.
|
|
49
|
+
|
|
50
|
+
| Profile field | Environment variable it reads |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| `apiKeyEnv` | API key |
|
|
53
|
+
| `apiBaseEnv` | Base URL (OpenAI-compatible endpoints) |
|
|
54
|
+
| `apiVersionEnv` | Optional API version, only for endpoints that require one |
|
|
55
|
+
|
|
56
|
+
## OpenAI
|
|
57
|
+
|
|
58
|
+
Point the profile at the public OpenAI API. Only a key is required.
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
export OPENAI_API_KEY='sk-…'
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```jsonc
|
|
65
|
+
{
|
|
66
|
+
"console": {
|
|
67
|
+
"defaultProfile": "openai",
|
|
68
|
+
"profiles": {
|
|
69
|
+
"openai": {
|
|
70
|
+
"label": "OpenAI",
|
|
71
|
+
"model": "openai/gpt-5-mini",
|
|
72
|
+
"apiKeyEnv": "OPENAI_API_KEY"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Anthropic
|
|
80
|
+
|
|
81
|
+
Only a key is required.
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
export ANTHROPIC_API_KEY='sk-ant-…'
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```jsonc
|
|
88
|
+
{
|
|
89
|
+
"console": {
|
|
90
|
+
"defaultProfile": "anthropic",
|
|
91
|
+
"profiles": {
|
|
92
|
+
"anthropic": {
|
|
93
|
+
"label": "Anthropic",
|
|
94
|
+
"model": "anthropic/claude-sonnet-4-5",
|
|
95
|
+
"apiKeyEnv": "ANTHROPIC_API_KEY"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## OpenAI-compatible
|
|
103
|
+
|
|
104
|
+
Use this one profile for any endpoint that speaks the OpenAI Chat Completions
|
|
105
|
+
API: Azure, Opencode, Ollama, LM Studio, a LiteLLM proxy, and so on. Set the
|
|
106
|
+
key and the base URL; the model name is whatever the endpoint expects.
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
export OPENAI_API_KEY='…'
|
|
110
|
+
export OPENAI_API_BASE='https://host.example/v1'
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```jsonc
|
|
114
|
+
{
|
|
115
|
+
"console": {
|
|
116
|
+
"defaultProfile": "openai-compatible",
|
|
117
|
+
"profiles": {
|
|
118
|
+
"openai-compatible": {
|
|
119
|
+
"label": "OpenAI-compatible",
|
|
120
|
+
"model": "openai/your-model-name",
|
|
121
|
+
"apiKeyEnv": "OPENAI_API_KEY",
|
|
122
|
+
"apiBaseEnv": "OPENAI_API_BASE"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
LiteLLM appends `/chat/completions` to `OPENAI_API_BASE`, so point it at the
|
|
130
|
+
`…/v1` root of the endpoint, not the full completions path.
|
|
131
|
+
|
|
132
|
+
### Azure
|
|
133
|
+
|
|
134
|
+
Azure OpenAI's unified v1 API is OpenAI-compatible, so it uses the same
|
|
135
|
+
`openai-compatible` profile. Use the **Azure deployment name** as the model.
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
export OPENAI_API_KEY='your-azure-openai-api-key'
|
|
139
|
+
export OPENAI_API_BASE='https://YOUR-RESOURCE-NAME.openai.azure.com/openai/v1/'
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```jsonc
|
|
143
|
+
{
|
|
144
|
+
"console": {
|
|
145
|
+
"defaultProfile": "openai-compatible",
|
|
146
|
+
"profiles": {
|
|
147
|
+
"openai-compatible": {
|
|
148
|
+
"label": "Azure OpenAI",
|
|
149
|
+
"model": "openai/gpt-4.1-nano", // your deployment name
|
|
150
|
+
"apiKeyEnv": "OPENAI_API_KEY",
|
|
151
|
+
"apiBaseEnv": "OPENAI_API_BASE"
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Notes:
|
|
159
|
+
|
|
160
|
+
- `OPENAI_API_BASE` also accepts the Foundry models form
|
|
161
|
+
`https://YOUR-RESOURCE-NAME.services.ai.azure.com/openai/v1/`.
|
|
162
|
+
- With the v1 API the `api-version` query parameter is no longer required, so
|
|
163
|
+
`apiVersionEnv` stays unset. If an older endpoint still needs one, add
|
|
164
|
+
`"apiVersionEnv": "OPENAI_API_VERSION"` and export
|
|
165
|
+
`OPENAI_API_VERSION='2024-06-01'` (or your API version).
|
|
166
|
+
- `OPENAI_API_KEY` holds the Azure OpenAI API key. If you use Microsoft Entra
|
|
167
|
+
ID instead, a keyless token provider can be passed in the same variable.
|
|
168
|
+
|
|
169
|
+
### Opencode
|
|
170
|
+
|
|
171
|
+
[OpenCode Go](https://opencode.ai) publishes an OpenAI-compatible API at
|
|
172
|
+
`https://opencode.ai/zen/go/v1`; its `GET /v1/models` listing is public and does
|
|
173
|
+
not require a key. The Console can use it directly, so most setups do not need a
|
|
174
|
+
local gateway.
|
|
175
|
+
|
|
176
|
+
The repository includes a discovery script that fetches the live model list and
|
|
177
|
+
merges ready-made Console profiles into `config.json`. From a Catence checkout:
|
|
178
|
+
|
|
179
|
+
```sh
|
|
180
|
+
npm run discover:opencode-go -- --write ~/.catence/config.json
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The script adds two profiles, because OpenCode Go routes model families through
|
|
184
|
+
different API shapes:
|
|
185
|
+
|
|
186
|
+
- `opencode-go` — chat and responses models, base
|
|
187
|
+
`https://opencode.ai/zen/go/v1` (LiteLLM `openai/…` names).
|
|
188
|
+
- `opencode-go-messages` — messages models, base `https://opencode.ai/zen/go`
|
|
189
|
+
(LiteLLM `anthropic/…` names).
|
|
190
|
+
|
|
191
|
+
| Family | Profile prefix | Example |
|
|
192
|
+
| --- | --- | --- |
|
|
193
|
+
| Chat | `openai/<id>` | `openai/deepseek-v4-flash` |
|
|
194
|
+
| Responses | `openai/responses/<id>` | `openai/responses/grok-4.5` |
|
|
195
|
+
| Messages | `anthropic/<id>` | `anthropic/minimax-m2.5` |
|
|
196
|
+
|
|
197
|
+
The script warns on stderr and skips any model that does not fit one of these
|
|
198
|
+
families. Rerun it to refresh the model lists; existing profiles, limits, and
|
|
199
|
+
`defaultProfile` are preserved unless `--set-default` is passed.
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
export OPENCODE_GO_API_KEY='…' # any non-empty value satisfies the key check
|
|
203
|
+
export OPENCODE_GO_API_BASE='https://opencode.ai/zen/go/v1'
|
|
204
|
+
export OPENCODE_GO_MESSAGES_API_BASE='https://opencode.ai/zen/go'
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
```jsonc
|
|
208
|
+
{
|
|
209
|
+
"console": {
|
|
210
|
+
"defaultProfile": "opencode-go",
|
|
211
|
+
"profiles": {
|
|
212
|
+
"opencode-go": {
|
|
213
|
+
"label": "OpenCode Go",
|
|
214
|
+
"defaultModel": "flash",
|
|
215
|
+
"models": {
|
|
216
|
+
"flash": { "label": "DeepSeek V4 flash", "model": "openai/deepseek-v4-flash" },
|
|
217
|
+
"grok": { "label": "Grok 4.5", "model": "openai/responses/grok-4.5" }
|
|
218
|
+
},
|
|
219
|
+
"apiKeyEnv": "OPENCODE_GO_API_KEY",
|
|
220
|
+
"apiBaseEnv": "OPENCODE_GO_API_BASE"
|
|
221
|
+
},
|
|
222
|
+
"opencode-go-messages": {
|
|
223
|
+
"label": "OpenCode Go messages",
|
|
224
|
+
"defaultModel": "minimax",
|
|
225
|
+
"models": {
|
|
226
|
+
"minimax": { "label": "MiniMax M2.5", "model": "anthropic/minimax-m2.5" }
|
|
227
|
+
},
|
|
228
|
+
"apiKeyEnv": "OPENCODE_GO_API_KEY",
|
|
229
|
+
"apiBaseEnv": "OPENCODE_GO_MESSAGES_API_BASE"
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Verify with `catence-console doctor`. The `responses` models
|
|
237
|
+
(`openai/responses/…`) go through LiteLLM's responses bridge and are the least
|
|
238
|
+
battle-tested path; smoke-test one chat turn before relying on them.
|
|
239
|
+
|
|
240
|
+
If you instead want to reuse the providers already configured in your local
|
|
241
|
+
Opencode CLI, run an OpenAI-compatible gateway such as the
|
|
242
|
+
[`opencode-llm-proxy`](https://github.com/KochC/opencode-llm-proxy) plugin, which
|
|
243
|
+
serves Chat Completions at `http://127.0.0.1:4010/v1` and lists every configured
|
|
244
|
+
model as `provider/model`.
|
|
245
|
+
|
|
246
|
+
```sh
|
|
247
|
+
export OPENAI_API_KEY='unused' # or your OPENCODE_LLM_PROXY_TOKEN
|
|
248
|
+
export OPENAI_API_BASE='http://127.0.0.1:4010/v1'
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
```jsonc
|
|
252
|
+
{
|
|
253
|
+
"console": {
|
|
254
|
+
"defaultProfile": "openai-compatible",
|
|
255
|
+
"profiles": {
|
|
256
|
+
"openai-compatible": {
|
|
257
|
+
"label": "Opencode",
|
|
258
|
+
"model": "openai/github-copilot/claude-sonnet-4.6", // provider/model
|
|
259
|
+
"apiKeyEnv": "OPENAI_API_KEY",
|
|
260
|
+
"apiBaseEnv": "OPENAI_API_BASE"
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Notes:
|
|
268
|
+
|
|
269
|
+
- Use the fully qualified `provider/model` id from the gateway's
|
|
270
|
+
`GET /v1/models` listing.
|
|
271
|
+
- If the gateway requires a bearer token, set `OPENAI_API_KEY` to the
|
|
272
|
+
`OPENCODE_LLM_PROXY_TOKEN`; otherwise any non-empty value satisfies the
|
|
273
|
+
profile's required-environment check.
|
|
274
|
+
|
|
275
|
+
### Opencode Zen
|
|
276
|
+
|
|
277
|
+
[OpenCode Zen](https://opencode.ai/zen) is a hosted model API with a curated
|
|
278
|
+
selection of coding and reasoning models. It exposes an OpenAI-compatible
|
|
279
|
+
endpoint at `https://opencode.ai/zen/v1` with a public `GET /v1/models`
|
|
280
|
+
listing. Several models are available for free (no billing details required);
|
|
281
|
+
paid models require an API key from [opencode.ai/auth](https://opencode.ai/auth).
|
|
282
|
+
|
|
283
|
+
The repository includes a discovery script that fetches the live model list and
|
|
284
|
+
merges ready-made Console profiles into `config.json`. From a Catence checkout:
|
|
285
|
+
|
|
286
|
+
```sh
|
|
287
|
+
npm run discover:opencode-zen -- --write ~/.catence/config.json
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Add `--free-only` to include only the free models.
|
|
291
|
+
|
|
292
|
+
The script adds two profiles, because OpenCode Zen routes model families through
|
|
293
|
+
different API shapes:
|
|
294
|
+
|
|
295
|
+
- `opencode-zen` — chat/completions models (most models, including all free
|
|
296
|
+
models), base `https://opencode.ai/zen/v1` (LiteLLM `openai/<id>` names).
|
|
297
|
+
- `opencode-zen-responses` — responses API models (GPT-5.x series), base
|
|
298
|
+
`https://opencode.ai/zen/v1` (LiteLLM `openai/responses/<id>` names).
|
|
299
|
+
|
|
300
|
+
| Family | Profile prefix | Example |
|
|
301
|
+
| --- | --- | --- |
|
|
302
|
+
| Chat | `openai/<id>` | `openai/deepseek-v4-flash-free` |
|
|
303
|
+
| Responses | `openai/responses/<id>` | `openai/responses/gpt-5.6-luna` |
|
|
304
|
+
|
|
305
|
+
Free models carry a `-free` suffix (e.g., `deepseek-v4-flash-free`,
|
|
306
|
+
`nemotron-3-ultra-free`, `big-pickle`). The discovery script recognises them
|
|
307
|
+
automatically; `--free-only` filters the list to free models only.
|
|
308
|
+
|
|
309
|
+
```sh
|
|
310
|
+
export OPENCODE_API_KEY='…' # required for paid models; any non-empty value for free
|
|
311
|
+
export OPENCODE_ZEN_API_BASE='https://opencode.ai/zen/v1'
|
|
312
|
+
export OPENCODE_ZEN_RESPONSES_API_BASE='https://opencode.ai/zen/v1'
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
```jsonc
|
|
316
|
+
{
|
|
317
|
+
"console": {
|
|
318
|
+
"defaultProfile": "opencode-zen",
|
|
319
|
+
"profiles": {
|
|
320
|
+
"opencode-zen": {
|
|
321
|
+
"label": "OpenCode Zen",
|
|
322
|
+
"defaultModel": "deepseek-v4-flash-free",
|
|
323
|
+
"models": {
|
|
324
|
+
"deepseek-v4-flash-free": { "label": "DeepSeek V4 Flash Free", "model": "openai/deepseek-v4-flash-free" },
|
|
325
|
+
"nemotron-3-ultra-free": { "label": "Nemotron 3 Ultra Free", "model": "openai/nemotron-3-ultra-free" },
|
|
326
|
+
"big-pickle": { "label": "Big Pickle", "model": "openai/big-pickle" }
|
|
327
|
+
},
|
|
328
|
+
"apiKeyEnv": "OPENCODE_API_KEY",
|
|
329
|
+
"apiBaseEnv": "OPENCODE_ZEN_API_BASE"
|
|
330
|
+
},
|
|
331
|
+
"opencode-zen-responses": {
|
|
332
|
+
"label": "OpenCode Zen (Responses)",
|
|
333
|
+
"defaultModel": "gpt-5.6-luna",
|
|
334
|
+
"models": {
|
|
335
|
+
"gpt-5.6-luna": { "label": "GPT 5.6 Luna", "model": "openai/responses/gpt-5.6-luna" },
|
|
336
|
+
"gpt-5.6-terra": { "label": "GPT 5.6 Terra", "model": "openai/responses/gpt-5.6-terra" }
|
|
337
|
+
},
|
|
338
|
+
"apiKeyEnv": "OPENCODE_API_KEY",
|
|
339
|
+
"apiBaseEnv": "OPENCODE_ZEN_RESPONSES_API_BASE"
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Verify with `catence-console doctor`. The `responses` models
|
|
347
|
+
(`openai/responses/…`) go through LiteLLM's responses bridge and are the least
|
|
348
|
+
battle-tested path; smoke-test one chat turn before relying on them.
|
|
349
|
+
|
|
350
|
+
The same `OPENCODE_API_KEY` works for both Opencode Go and Opencode Zen.
|
|
351
|
+
|
|
352
|
+
## Multiple deployments per provider
|
|
353
|
+
|
|
354
|
+
A profile can list several models under one set of credentials; the Console
|
|
355
|
+
settings then offer a per-deployment choice.
|
|
356
|
+
|
|
357
|
+
```jsonc
|
|
358
|
+
{
|
|
359
|
+
"console": {
|
|
360
|
+
"profiles": {
|
|
361
|
+
"openai-compatible": {
|
|
362
|
+
"label": "Azure OpenAI",
|
|
363
|
+
"defaultModel": "nano",
|
|
364
|
+
"models": {
|
|
365
|
+
"nano": { "label": "GPT-4.1 nano", "model": "openai/gpt-4.1-nano" },
|
|
366
|
+
"mini": { "label": "GPT-5 mini", "model": "openai/gpt-5-mini" }
|
|
367
|
+
},
|
|
368
|
+
"apiKeyEnv": "OPENAI_API_KEY",
|
|
369
|
+
"apiBaseEnv": "OPENAI_API_BASE"
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## Reasoning effort
|
|
377
|
+
|
|
378
|
+
Models expose a per-chat **Thinking effort** dropdown in the Console settings.
|
|
379
|
+
By default it offers the OpenAI-standard levels — `minimal`, `low`, `medium`,
|
|
380
|
+
`high`, `xhigh` — which suit OpenAI and most OpenAI-compatible endpoints.
|
|
381
|
+
|
|
382
|
+
Some models (for example DeepSeek V4 Flash) use their own reasoning-effort
|
|
383
|
+
scheme rather than the OpenAI-standard list. For those, declare `variants` on
|
|
384
|
+
the model: a mapping of dropdown label to the exact value sent to the provider.
|
|
385
|
+
The chat dropdown then shows `Provider default` plus each label instead of the
|
|
386
|
+
OpenAI-standard list.
|
|
387
|
+
|
|
388
|
+
```jsonc
|
|
389
|
+
{
|
|
390
|
+
"console": {
|
|
391
|
+
"profiles": {
|
|
392
|
+
"opencode-zen": {
|
|
393
|
+
"label": "OpenCode Zen",
|
|
394
|
+
"defaultModel": "deepseek-v4-flash-free",
|
|
395
|
+
"models": {
|
|
396
|
+
"deepseek-v4-flash-free": {
|
|
397
|
+
"label": "DeepSeek V4 Flash Free",
|
|
398
|
+
"model": "openai/deepseek-v4-flash-free",
|
|
399
|
+
"variants": { "High": "high", "Max": "max" }
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
"apiKeyEnv": "OPENCODE_API_KEY",
|
|
403
|
+
"apiBaseEnv": "OPENCODE_ZEN_API_BASE"
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Selecting a variant sends that value verbatim as `reasoning_effort` (with
|
|
411
|
+
`allowed_openai_params` so LiteLLM forwards it for OpenAI-compatible models).
|
|
412
|
+
The `Provider default` option sends no reasoning effort at all.
|
|
413
|
+
|
|
414
|
+
An explicitly empty `variants` map disables reasoning effort for that model:
|
|
415
|
+
no `reasoning_effort` is sent (the agent guards against it) and the chat
|
|
416
|
+
**Thinking effort** dropdown is disabled in the UI. This suits models that
|
|
417
|
+
reject the parameter, such as MiMo, which expects its own `thinking` toggle
|
|
418
|
+
instead. `variants: {}` cannot be combined with a fixed `reasoningEffort` on
|
|
419
|
+
the same model.
|
|
420
|
+
|
|
421
|
+
You can also pin a fixed level with `reasoningEffort` on the model, or a
|
|
422
|
+
profile-wide default with `defaultReasoningEffort` (OpenAI-standard values
|
|
423
|
+
only). Precedence: per-chat dropdown selection > model `reasoningEffort` >
|
|
424
|
+
profile `defaultReasoningEffort` > provider default. The **Thinking effort**
|
|
425
|
+
dropdown is rebuilt whenever the **Model** dropdown changes, so its options
|
|
426
|
+
(and disabled state) always reflect the selected model.
|
|
427
|
+
|
|
428
|
+
## Verifying a profile
|
|
429
|
+
|
|
430
|
+
`catence-console doctor` reports, for each profile, which environment variables
|
|
431
|
+
are still missing and whether the local Catence runtime is reachable. It never
|
|
432
|
+
prints credential values.
|
|
433
|
+
|
|
434
|
+
```sh
|
|
435
|
+
catence-console doctor --home "$HOME/.catence" --mcp-url http://127.0.0.1:8787/mcp
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
On Docker, the deploy scaffold includes a helper that runs it inside the
|
|
439
|
+
running container (`exec`, not `run` — the container's loopback hosts the live
|
|
440
|
+
MCP server and the `.env` provider keys are in its environment):
|
|
441
|
+
|
|
442
|
+
```sh
|
|
443
|
+
./catence-deploy/doctor.sh
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
## Docker: generating the Console login hash
|
|
447
|
+
|
|
448
|
+
`deploy-console.sh` writes a small helper into the deployment directory. Run it
|
|
449
|
+
once after the script to generate the bcrypt hash for
|
|
450
|
+
`CATENCE_CONSOLE_PASSWORD_HASH` — it prompts for the password on the terminal,
|
|
451
|
+
never echoes it, and uses the compose project so it works no matter what image
|
|
452
|
+
tag the script built:
|
|
453
|
+
|
|
454
|
+
```sh
|
|
455
|
+
./catence-deploy/hash-password.sh
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Paste the output into `CATENCE_CONSOLE_PASSWORD_HASH` in `catence-deploy/.env`,
|
|
459
|
+
then start the stack (`./deploy-console.sh beta` again, or
|
|
460
|
+
`docker compose -f catence-deploy/docker-compose.yml --env-file catence-deploy/.env up -d`).
|
|
461
|
+
|
|
462
|
+
If you prefer, re-run the deploy script with `--generate-secrets` instead and
|
|
463
|
+
answer the interactive password prompt; it saves the hash to `.env` for you.
|
|
464
|
+
Without the helper (for example an older deployment), the equivalent command
|
|
465
|
+
is `docker run --rm -it --entrypoint /opt/catence-console/bin/catence-console
|
|
466
|
+
<image-tag> auth hash-password`, where `<image-tag>` is the tag printed by the
|
|
467
|
+
script.
|