@assertrx/dino 1.0.1 → 1.0.2

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 CHANGED
@@ -321,8 +321,9 @@ npm install -g @earendil-works/pi-coding-agent
321
321
  # 2. Install Dino via pi
322
322
  pi install npm:@assertrx/dino --approve
323
323
 
324
- # 3. Set API key
324
+ # 3. Set an API key (any supported provider works)
325
325
  export ANTHROPIC_API_KEY=sk-ant-...
326
+ # e.g. for OpenAI instead: export OPENAI_API_KEY=sk-...
326
327
 
327
328
  # 4. Start the server
328
329
  node ~/.pi/agent/npm/node_modules/@assertrx/dino/scripts/api-server.mjs
@@ -333,13 +334,35 @@ node ~/.pi/agent/npm/node_modules/@assertrx/dino/scripts/api-server.mjs
333
334
  | Method | Path | Description |
334
335
  |--------|------|-------------|
335
336
  | `GET` | `/api/health` | Health check |
337
+ | `GET` | `/api/providers` | List all providers with auth status and model counts |
338
+ | `GET` | `/api/models` | List models available with current credentials (`?provider=<id>` to filter) |
339
+ | `POST` | `/api/models` | List models for given credentials — body accepts `provider`+`apiKey` or `apiKeys` (validates the key and unlocks that provider's catalog) |
336
340
  | `GET` | `/api/prompts` | List all available prompts with parameters |
337
341
  | `GET` | `/api/prompts/:name` | Get raw markdown content of a prompt |
338
342
  | `POST` | `/api/prompts/:name` | **Execute** a prompt via the pi agent |
339
343
 
344
+ ### Providers, models & API keys
345
+
346
+ The server supports every pi provider (Anthropic, OpenAI, Google, DeepSeek, Groq, Mistral, xAI, OpenRouter, and many more). Credentials can come from three places, in this order of precedence:
347
+
348
+ 1. **Per-request keys** — pass `apiKeys` (a map of provider → key) or `apiKey` (+ optional `provider`) in the POST body. These keys are scoped to that single request's session.
349
+ 2. **Environment variables** — any provider's standard env var (e.g. `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`).
350
+ 3. **Stored credentials** — logins created via `pi /login` in `~/.pi/agent/auth.json`.
351
+
352
+ Use `GET /api/providers` to discover which providers are configured and `GET /api/models` to list models available to the server right now.
353
+
354
+ **Selecting a model:** pass `model` in the POST body as either `"provider/model-id"` (e.g. `"openai/gpt-5.2"`) or `{ "provider": "openai", "id": "gpt-5.2" }`. Without `model`, the session uses the default model resolution (configured provider defaults). Requesting a model whose provider has no credentials returns a clear error.
355
+
340
356
  ### Prompt execution endpoints
341
357
 
342
- All POST bodies accept `apiKey` (optional, falls back to `ANTHROPIC_API_KEY` env var), `cwd` (workspace directory, defaults to current), and `stream` (default: `true` for SSE).
358
+ All POST bodies accept auth/model options, `cwd` (workspace directory, defaults to current), and `stream` (default: `true` for SSE):
359
+
360
+ | Body param | Description |
361
+ |------------|-------------|
362
+ | `apiKeys` | Map of provider → API key, e.g. `{"anthropic": "sk-...", "openai": "sk-..."}` |
363
+ | `apiKey` | Single API key. Pair with `provider` to target one explicitly; otherwise applied to the first available provider (anthropic fallback) |
364
+ | `provider` | Provider id (e.g. `"openai"`) to pair with a single `apiKey` |
365
+ | `model` | Model selection: `"provider/model-id"`, `"model-id"`, or `{"provider": ..., "id": ...}` |
343
366
 
344
367
  | Prompt name | Required params |
345
368
  |-------------|---------------|
@@ -357,14 +380,33 @@ All POST bodies accept `apiKey` (optional, falls back to `ANTHROPIC_API_KEY` env
357
380
  # List all prompts
358
381
  curl http://localhost:3000/api/prompts
359
382
 
360
- # Get prompt details
361
- curl http://localhost:3000/api/prompts/setup-playwright
383
+ # Discover providers and their auth status
384
+ curl http://localhost:3000/api/providers
385
+
386
+ # List models available to the server (optionally per provider)
387
+ curl http://localhost:3000/api/models
388
+ curl "http://localhost:3000/api/models?provider=anthropic"
362
389
 
363
- # Execute a prompt (SSE streaming)
390
+ # Execute a prompt (SSE streaming, auth from env vars)
364
391
  curl -X POST http://localhost:3000/api/prompts/setup-playwright \
365
392
  -H "Content-Type: application/json" \
366
393
  -d '{"cwd": "/path/to/workspace"}'
367
394
 
395
+ # Execute with a single provider key + model selection
396
+ # (env: export OPENAI_API_KEY=sk-... then:)
397
+ curl -X POST http://localhost:3000/api/prompts/setup-playwright \
398
+ -H "Content-Type: application/json" \
399
+ -d '{"cwd": "/path/to/workspace", "model": "openai/gpt-5.2"}'
400
+
401
+ # Execute with multiple per-request API keys and an explicit model
402
+ curl -X POST http://localhost:3000/api/prompts/setup-playwright \
403
+ -H "Content-Type: application/json" \
404
+ -d '{
405
+ "cwd": "/path/to/workspace",
406
+ "apiKeys": {"anthropic": "sk-ant-...", "openai": "sk-ov-..."},
407
+ "model": "anthropic/claude-sonnet-4-5"
408
+ }'
409
+
368
410
  # Execute with URL parameter (non-streaming JSON response)
369
411
  curl -X POST http://localhost:3000/api/prompts/generate-pom-from-url-cli \
370
412
  -H "Content-Type: application/json" \
@@ -399,5 +441,5 @@ event: error → {"message": "..."} # error
399
441
  |----------|---------|-------------|
400
442
  | `PORT` | `3000` | Server port |
401
443
  | `HOST` | `0.0.0.0` | Server host |
402
- | `ANTHROPIC_API_KEY` | — | API key (or pass `apiKey` in request body) |
444
+ | Provider env vars | — | Any provider's standard env var (e.g. `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`); per-request `apiKeys`/`apiKey` override these |
403
445
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assertrx/dino",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "A friendly purple dinosaur pi package with a custom TUI header, theme, and Playwright skills.",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -100,25 +100,119 @@ async function loadPrompt(name, params = {}) {
100
100
  return content;
101
101
  }
102
102
 
103
- /** Create a single-use pi agent session. */
104
- async function createSession({ cwd, apiKey }) {
105
- const modelRuntime = await ModelRuntime.create({
103
+ /**
104
+ * Create a fresh ModelRuntime.
105
+ *
106
+ * Runtime API keys set on this instance are isolated to the current request.
107
+ */
108
+ async function createModelRuntime() {
109
+ return ModelRuntime.create({
106
110
  allowModelNetwork: false,
107
111
  });
112
+ }
113
+
114
+ /**
115
+ * Resolve runtime API keys from the request body.
116
+ *
117
+ * Supports three styles:
118
+ * - `apiKeys`: { "anthropic": "...", "openai": "..." }
119
+ * - `provider` + `apiKey`: set one key for an explicit provider
120
+ * - `apiKey` only (legacy): set key for the first available provider, falling back to anthropic
121
+ */
122
+ function resolveApiKeys(body, availableModels) {
123
+ const keys = [];
124
+
125
+ if (body.apiKeys && typeof body.apiKeys === "object") {
126
+ for (const [providerId, key] of Object.entries(body.apiKeys)) {
127
+ if (typeof key === "string" && key.trim().length > 0) {
128
+ keys.push({ provider: providerId, key });
129
+ }
130
+ }
131
+ return keys;
132
+ }
133
+
134
+ if (body.apiKey && typeof body.apiKey === "string" && body.apiKey.trim().length > 0) {
135
+ const provider = body.provider && typeof body.provider === "string"
136
+ ? body.provider
137
+ : availableModels.length > 0
138
+ ? availableModels[0].provider
139
+ : "anthropic";
140
+ keys.push({ provider, key: body.apiKey });
141
+ }
142
+
143
+ return keys;
144
+ }
108
145
 
109
- // Set API key if provided (uses env vars otherwise)
110
- if (apiKey) {
111
- // Try to set for the first available provider (anthropic preferred)
112
- const available = await modelRuntime.getAvailable();
113
- if (available.length > 0) {
114
- const providerId = available[0].model.provider;
115
- await modelRuntime.setRuntimeApiKey(providerId, apiKey);
146
+ /**
147
+ * Resolve a model object from the request body.
148
+ *
149
+ * Accepts:
150
+ * - string: "anthropic/claude-opus-4-5" or "claude-opus-4-5"
151
+ * - object: { provider: "anthropic", id: "claude-opus-4-5" }
152
+ *
153
+ * If no model is requested, returns undefined so the agent picks a default.
154
+ */
155
+ function resolveModel(modelRuntime, modelSelector, configuredProviders) {
156
+ if (!modelSelector) return undefined;
157
+
158
+ let providerId;
159
+ let modelId;
160
+
161
+ if (typeof modelSelector === "string") {
162
+ const parts = modelSelector.split("/");
163
+ if (parts.length === 2) {
164
+ [providerId, modelId] = parts;
116
165
  } else {
117
- // Blindly try anthropic
118
- await modelRuntime.setRuntimeApiKey("anthropic", apiKey);
166
+ modelId = modelSelector;
167
+ providerId = configuredProviders[0] ?? "anthropic";
119
168
  }
169
+ } else if (typeof modelSelector === "object") {
170
+ providerId = modelSelector.provider;
171
+ modelId = modelSelector.id ?? modelSelector.modelId;
172
+ }
173
+
174
+ if (!providerId || !modelId) {
175
+ throw new Error(`Invalid model selector: ${JSON.stringify(modelSelector)}`);
176
+ }
177
+
178
+ const model = modelRuntime.getModel(providerId, modelId);
179
+ if (!model) {
180
+ throw new Error(`Model not found: ${providerId}/${modelId}`);
120
181
  }
121
182
 
183
+ if (!modelRuntime.hasConfiguredAuth(providerId)) {
184
+ throw new Error(
185
+ `Provider "${providerId}" is not configured with an API key. ` +
186
+ `Provide an apiKey or apiKeys for this provider.`
187
+ );
188
+ }
189
+
190
+ return model;
191
+ }
192
+
193
+ /** Create a single-use pi agent session. */
194
+ async function createSession({ cwd, apiKey, apiKeys, provider, model: modelSelector }) {
195
+ const modelRuntime = await createModelRuntime();
196
+
197
+ // Resolve keys before selecting a model so we know which providers are configured.
198
+ const availableBeforeKeys = modelRuntime.getAvailableSnapshot();
199
+ const keysToSet = resolveApiKeys({ apiKey, apiKeys, provider }, availableBeforeKeys);
200
+
201
+ // Set all runtime API keys in parallel.
202
+ await Promise.all(
203
+ keysToSet.map(({ provider: providerId, key }) =>
204
+ modelRuntime.setRuntimeApiKey(providerId, key).catch((err) => {
205
+ throw new Error(`Failed to set API key for ${providerId}: ${err.message}`);
206
+ })
207
+ )
208
+ );
209
+
210
+ // Refresh availability after keys are set so the model picker sees configured providers.
211
+ await modelRuntime.getAvailable();
212
+
213
+ const configuredProviders = keysToSet.map((entry) => entry.provider);
214
+ const model = resolveModel(modelRuntime, modelSelector, configuredProviders);
215
+
122
216
  const agentDir = getAgentDir();
123
217
 
124
218
  const loader = new DefaultResourceLoader({
@@ -133,6 +227,7 @@ async function createSession({ cwd, apiKey }) {
133
227
  modelRuntime,
134
228
  resourceLoader: loader,
135
229
  sessionManager: SessionManager.inMemory(),
230
+ ...(model ? { model } : {}),
136
231
  });
137
232
 
138
233
  return session;
@@ -185,6 +280,88 @@ app.get("/api/prompts/:name", async (request, reply) => {
185
280
  };
186
281
  });
187
282
 
283
+ // List providers and their auth/configuration status
284
+ app.get("/api/providers", async (request, reply) => {
285
+ try {
286
+ const modelRuntime = await createModelRuntime();
287
+ const providers = modelRuntime.getProviders().map((provider) => ({
288
+ id: provider.id,
289
+ name: provider.name ?? provider.id,
290
+ authStatus: modelRuntime.getProviderAuthStatus(provider.id),
291
+ modelCount: modelRuntime.getModels(provider.id).length,
292
+ }));
293
+ return { providers };
294
+ } catch (err) {
295
+ reply.code(500);
296
+ return { error: `Failed to list providers: ${err.message}` };
297
+ }
298
+ });
299
+
300
+ // List available models. Optionally filter with ?provider=<id>.
301
+ app.get("/api/models", async (request, reply) => {
302
+ try {
303
+ const { provider } = request.query;
304
+ const modelRuntime = await createModelRuntime();
305
+ await modelRuntime.getAvailable();
306
+ const models = modelRuntime.getAvailableSnapshot();
307
+ const filtered = provider
308
+ ? models.filter((model) => model.provider === provider)
309
+ : models;
310
+ return {
311
+ models: filtered.map((model) => ({
312
+ provider: model.provider,
313
+ id: model.id,
314
+ name: model.name ?? model.id,
315
+ thinking: model.thinking,
316
+ })),
317
+ };
318
+ } catch (err) {
319
+ reply.code(500);
320
+ return { error: `Failed to list models: ${err.message}` };
321
+ }
322
+ });
323
+
324
+ // List models available for the provided credentials (per-request keys).
325
+ // Accepts provider+apiKey or an apiKeys map; without keys, lists models from
326
+ // env/stored credentials. Used by clients to validate a key and pick a model.
327
+ app.post("/api/models", async (request, reply) => {
328
+ try {
329
+ const { provider, apiKey, apiKeys } = request.body || {};
330
+ const modelRuntime = await createModelRuntime();
331
+
332
+ const availableBeforeKeys = modelRuntime.getAvailableSnapshot();
333
+ const keysToSet = resolveApiKeys({ provider, apiKey, apiKeys }, availableBeforeKeys);
334
+
335
+ await Promise.all(
336
+ keysToSet.map(({ provider: providerId, key }) =>
337
+ modelRuntime.setRuntimeApiKey(providerId, key).catch((err) => {
338
+ throw new Error(`Failed to set API key for ${providerId}: ${err.message}`);
339
+ })
340
+ )
341
+ );
342
+
343
+ await modelRuntime.getAvailable();
344
+
345
+ let models = modelRuntime.getAvailableSnapshot();
346
+ if (provider) {
347
+ models = models.filter((model) => model.provider === provider);
348
+ }
349
+
350
+ return {
351
+ providers: [...new Set(keysToSet.map((entry) => entry.provider))],
352
+ models: models.map((model) => ({
353
+ provider: model.provider,
354
+ id: model.id,
355
+ name: model.name ?? model.id,
356
+ thinking: model.thinking,
357
+ })),
358
+ };
359
+ } catch (err) {
360
+ reply.code(500);
361
+ return { error: `Failed to list models: ${err.message}` };
362
+ }
363
+ });
364
+
188
365
  // Execute a prompt (SSE streaming)
189
366
  app.post("/api/prompts/:name", async (request, reply) => {
190
367
  const { name } = request.params;
@@ -196,6 +373,9 @@ app.post("/api/prompts/:name", async (request, reply) => {
196
373
 
197
374
  const {
198
375
  apiKey,
376
+ apiKeys,
377
+ provider,
378
+ model,
199
379
  cwd = process.cwd(),
200
380
  stream = true,
201
381
  ...params
@@ -221,7 +401,13 @@ app.post("/api/prompts/:name", async (request, reply) => {
221
401
  // Create session
222
402
  let session;
223
403
  try {
224
- session = await createSession({ cwd: resolve(cwd), apiKey });
404
+ session = await createSession({
405
+ cwd: resolve(cwd),
406
+ apiKey,
407
+ apiKeys,
408
+ provider,
409
+ model,
410
+ });
225
411
  } catch (err) {
226
412
  reply.code(500);
227
413
  return { error: `Failed to create agent session: ${err.message}` };
@@ -313,7 +499,9 @@ try {
313
499
  await app.listen({ port: PORT, host: HOST });
314
500
  console.log(`🦕 Dino API server running at http://${HOST}:${PORT}`);
315
501
  console.log(` Prompts: http://${HOST}:${PORT}/api/prompts`);
502
+ console.log(` Providers: http://${HOST}:${PORT}/api/providers`);
503
+ console.log(` Models: http://${HOST}:${PORT}/api/models`);
316
504
  } catch (err) {
317
505
  app.log.error(err);
318
506
  process.exit(1);
319
- }
507
+ }
@@ -2,51 +2,51 @@
2
2
  setlocal enabledelayedexpansion
3
3
 
4
4
  echo ============================================
5
- echo Dino API Server - Setup ^& Start
5
+ echo Dino API Server - Setup ^& Start
6
6
  echo ============================================
7
7
  echo.
8
8
 
9
9
  :: ── 1. Check Node.js ──────────────────────────────────────────────────
10
10
  echo [1/4] Checking Node.js...
11
11
  where node >nul 2>&1
12
- if %ERRORLEVEL% NEQ 0 (
12
+ if !ERRORLEVEL! NEQ 0 (
13
13
  echo ERROR: Node.js is not installed.
14
14
  echo Please install Node.js from https://nodejs.org/ and re-run this script.
15
15
  pause
16
16
  exit /b 1
17
17
  )
18
- for /f "tokens=*" %%v in ('node -v') do echo Found Node.js %%v
18
+ for /f "tokens=*" %%v in ('node -v') do echo Found Node.js %%v
19
19
 
20
20
  :: ── 2. Install pi (if missing) ────────────────────────────────────────
21
21
  echo [2/4] Checking pi CLI...
22
22
  where pi >nul 2>&1
23
- if %ERRORLEVEL% EQU 0 (
24
- for /f "tokens=*" %%v in ('pi --version 2^>^&1') do echo Found pi %%v
23
+ if !ERRORLEVEL! EQU 0 (
24
+ for /f "tokens=*" %%v in ('pi --version 2^>^&1') do echo Found pi %%v
25
25
  ) else (
26
- echo Installing pi...
26
+ echo Installing pi...
27
27
  call npm install -g @earendil-works/pi-coding-agent
28
- if %ERRORLEVEL% NEQ 0 (
28
+ if !ERRORLEVEL! NEQ 0 (
29
29
  echo ERROR: Failed to install pi.
30
30
  pause
31
31
  exit /b 1
32
32
  )
33
- echo pi installed successfully.
33
+ echo pi installed successfully.
34
34
  )
35
35
 
36
36
  :: ── 3. Install Dino via pi (if missing) ───────────────────────────────
37
37
  echo [3/4] Checking Dino package...
38
38
  set "DINO_DIR=%USERPROFILE%\.pi\agent\npm\node_modules\@assertrx\dino"
39
39
  if not exist "%DINO_DIR%" (
40
- echo Installing @assertrx/dino via pi...
40
+ echo Installing @assertrx/dino via pi...
41
41
  call pi install npm:@assertrx/dino --approve
42
- if %ERRORLEVEL% NEQ 0 (
42
+ if !ERRORLEVEL! NEQ 0 (
43
43
  echo ERROR: Failed to install Dino package.
44
44
  pause
45
45
  exit /b 1
46
46
  )
47
- echo Dino package installed.
47
+ echo Dino package installed.
48
48
  ) else (
49
- echo Dino already installed at %DINO_DIR%
49
+ echo Dino already installed at %DINO_DIR%
50
50
  )
51
51
 
52
52
  :: ── 4. Start server ───────────────────────────────────────────────────
@@ -57,22 +57,22 @@ echo.
57
57
  if "%PORT%"=="" set PORT=3000
58
58
  if "%HOST%"=="" set HOST=0.0.0.0
59
59
 
60
- echo Port: %PORT%
61
- echo Host: %HOST%
60
+ echo Port: %PORT%
61
+ echo Host: %HOST%
62
62
  echo.
63
63
 
64
64
  :: Use ANTHROPIC_API_KEY from env if set
65
65
  if defined ANTHROPIC_API_KEY (
66
- echo API key: loaded from ANTHROPIC_API_KEY
66
+ echo API key: loaded from ANTHROPIC_API_KEY
67
67
  ) else (
68
- echo WARNING: No ANTHROPIC_API_KEY set. Set it or pass apiKey in request body.
68
+ echo WARNING: No ANTHROPIC_API_KEY set. Set it or pass apiKey in request body.
69
69
  )
70
70
 
71
71
  echo.
72
- echo Endpoints:
73
- echo GET http://%HOST%:%PORT%/api/health
74
- echo GET http://%HOST%:%PORT%/api/prompts
75
- echo POST http://%HOST%:%PORT%/api/prompts/:name
72
+ echo Endpoints:
73
+ echo GET http://%HOST%:%PORT%/api/health
74
+ echo GET http://%HOST%:%PORT%/api/prompts
75
+ echo POST http://%HOST%:%PORT%/api/prompts/:name
76
76
  echo.
77
77
  echo ============================================
78
78
  echo.
@@ -83,11 +83,11 @@ set "LOCAL_API=%~dp0..\scripts\api-server.mjs"
83
83
  set "INSTALLED_API=%DINO_DIR%\scripts\api-server.mjs"
84
84
 
85
85
  if exist "%INSTALLED_API%" (
86
- echo Using installed api-server.mjs
86
+ echo Using installed api-server.mjs
87
87
  node "%INSTALLED_API%"
88
88
  ) else if exist "%LOCAL_API%" (
89
- echo Installed api-server.mjs not found.
90
- echo Falling back to local source: %LOCAL_API%
89
+ echo Installed api-server.mjs not found.
90
+ echo Falling back to local source: %LOCAL_API%
91
91
  node "%LOCAL_API%"
92
92
  ) else (
93
93
  echo ERROR: api-server.mjs not found at either of the following locations: