create-walle 0.9.21 → 0.9.22
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 +5 -5
- package/package.json +2 -2
- package/template/claude-task-manager/api-prompts.js +13 -0
- package/template/claude-task-manager/api-reviews.js +5 -2
- package/template/claude-task-manager/db.js +348 -15
- package/template/claude-task-manager/docs/app-update-refresh-protocol.md +69 -0
- package/template/claude-task-manager/docs/image-paste-ux.md +3 -0
- package/template/claude-task-manager/docs/ipad-web-preview.md +88 -0
- package/template/claude-task-manager/git-utils.js +146 -17
- package/template/claude-task-manager/lib/auth-rate-limit.js +23 -3
- package/template/claude-task-manager/lib/auth-rules.js +3 -0
- package/template/claude-task-manager/lib/document-review.js +33 -2
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +83 -0
- package/template/claude-task-manager/lib/mobile-auth-api.js +14 -0
- package/template/claude-task-manager/lib/restart-guard.js +68 -0
- package/template/claude-task-manager/lib/session-standup.js +36 -13
- package/template/claude-task-manager/lib/session-stream.js +11 -4
- package/template/claude-task-manager/lib/transport-security.js +50 -0
- package/template/claude-task-manager/lib/walle-transcript.js +16 -0
- package/template/claude-task-manager/lib/worktree-active-sync.js +6 -3
- package/template/claude-task-manager/public/css/reviews.css +10 -0
- package/template/claude-task-manager/public/css/setup.css +13 -0
- package/template/claude-task-manager/public/css/walle.css +145 -0
- package/template/claude-task-manager/public/index.html +539 -44
- package/template/claude-task-manager/public/ipad.html +363 -0
- package/template/claude-task-manager/public/js/document-review-links.js +196 -0
- package/template/claude-task-manager/public/js/message-renderer.js +14 -3
- package/template/claude-task-manager/public/js/reviews.js +30 -6
- package/template/claude-task-manager/public/js/setup.js +42 -2
- package/template/claude-task-manager/public/js/stream-view.js +20 -1
- package/template/claude-task-manager/public/js/walle.js +314 -18
- package/template/claude-task-manager/public/m/app.css +789 -11
- package/template/claude-task-manager/public/m/app.js +1070 -67
- package/template/claude-task-manager/public/m/claim.html +9 -2
- package/template/claude-task-manager/public/m/index.html +17 -10
- package/template/claude-task-manager/public/m/sw.js +1 -1
- package/template/claude-task-manager/server.js +365 -95
- package/template/claude-task-manager/session-integrity.js +4 -0
- package/template/docs/designs/2026-05-17-portkey-gateway-provider-ux.md +86 -35
- package/template/package.json +1 -1
- package/template/wall-e/api-walle.js +19 -1
- package/template/wall-e/brain.js +152 -6
- package/template/wall-e/chat.js +85 -0
- package/template/wall-e/coding-orchestrator.js +106 -12
- package/template/wall-e/http/model-admin.js +131 -0
- package/template/wall-e/lib/service-health.js +194 -0
- package/template/wall-e/llm/anthropic.js +7 -0
- package/template/wall-e/llm/client.js +46 -12
- package/template/wall-e/llm/openai.js +17 -2
- package/template/wall-e/llm/portkey-sync.js +201 -0
- package/template/wall-e/server.js +13 -0
- package/template/website/index.html +10 -10
|
@@ -1277,12 +1277,16 @@ function repairSafeRelinkSwaps(db, auditResult = null) {
|
|
|
1277
1277
|
const projectA = rowA.project_path || stA.cwd || ctmRowA.cwd || '';
|
|
1278
1278
|
const projectB = rowB.project_path || stB.cwd || ctmRowB.cwd || '';
|
|
1279
1279
|
_updateColumns(db, 'startup_tasks', 'ctm_session_id', ctmA, {
|
|
1280
|
+
agent_session_id: desiredA,
|
|
1281
|
+
claude_session_id: desiredA,
|
|
1280
1282
|
agent_project_dir: _dirForJsonl(rowA.jsonl_path),
|
|
1281
1283
|
claude_project_dir: _dirForJsonl(rowA.jsonl_path),
|
|
1282
1284
|
cwd: projectA,
|
|
1283
1285
|
worktree_path: /\/\.(?:claude|walle)\/worktrees\//.test(projectA) ? projectA : null,
|
|
1284
1286
|
});
|
|
1285
1287
|
_updateColumns(db, 'startup_tasks', 'ctm_session_id', ctmB, {
|
|
1288
|
+
agent_session_id: desiredB,
|
|
1289
|
+
claude_session_id: desiredB,
|
|
1286
1290
|
agent_project_dir: _dirForJsonl(rowB.jsonl_path),
|
|
1287
1291
|
claude_project_dir: _dirForJsonl(rowB.jsonl_path),
|
|
1288
1292
|
cwd: projectB,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Portkey Gateway Provider UX
|
|
2
2
|
|
|
3
3
|
Date: 2026-05-17
|
|
4
|
-
|
|
4
|
+
Updated: 2026-05-20
|
|
5
|
+
Status: revised and implemented
|
|
5
6
|
|
|
6
7
|
## Context
|
|
7
8
|
|
|
@@ -14,16 +15,46 @@ Wall-E already has two separate ideas that should not be collapsed:
|
|
|
14
15
|
|
|
15
16
|
Portkey belongs on a third axis: **connection / transport**. A provider instance can be "OpenAI via Portkey" or "Anthropic via Portkey" while still using the OpenAI or Anthropic runtime adapter.
|
|
16
17
|
|
|
17
|
-
##
|
|
18
|
+
## Research Notes
|
|
19
|
+
|
|
20
|
+
Portkey's current public docs support the implementation shape:
|
|
21
|
+
|
|
22
|
+
- The OpenAI-compatible setup is still "change base URL to `https://api.portkey.ai/v1` and use a Portkey API key".
|
|
23
|
+
- The universal API supports OpenAI Chat Completions, OpenAI Responses, and Anthropic Messages against the gateway, so Portkey should be modeled as transport rather than as a standalone upstream provider.
|
|
24
|
+
- The model listing endpoint is `GET /v1/models`, requires `x-portkey-api-key`, and can filter by `provider`/`ai_service`. Its response includes provider-qualified ids such as `@ai-provider-slug/gpt-5`, which CTM must preserve instead of filtering through the built-in catalog.
|
|
25
|
+
|
|
26
|
+
## Revised UX Model
|
|
18
27
|
|
|
19
28
|
The `#models` page should use this vocabulary:
|
|
20
29
|
|
|
21
|
-
- **Provider**: upstream API/runtime family. Examples: OpenAI, Anthropic, Google Gemini.
|
|
30
|
+
- **Provider**: upstream API/runtime family. Examples: OpenAI, Anthropic, Google Gemini. Provider cards are the primary overview because CTM has few provider families.
|
|
22
31
|
- **Connection**: how requests reach the provider. Examples: Direct API key, Portkey gateway, Claude CLI subscription, Codex CLI subscription, OAuth proxy.
|
|
23
32
|
- **Route**: a concrete model plus provider instance. Examples: `gpt-5.5 · OpenAI · Direct`, `gpt-5.5 · OpenAI · Portkey prod`.
|
|
33
|
+
- **Gateway**: shared transport/account configuration that can be enabled per provider family. Portkey is the first gateway.
|
|
34
|
+
- **Model Catalog**: the long model inventory underneath provider cards. It is collapsed by default because the provider view is the decision surface.
|
|
24
35
|
|
|
25
36
|
This keeps the user from seeing Portkey as a competing "model provider" next to OpenAI and Anthropic. It also explains why a Claude model can be reachable through Portkey while Claude CLI remains a separate subscription-backed route.
|
|
26
37
|
|
|
38
|
+
## 2026-05-20 Product Decision
|
|
39
|
+
|
|
40
|
+
Portkey is enabled and disabled at the **provider** level, not the individual model level.
|
|
41
|
+
|
|
42
|
+
The provider card owns the access policy:
|
|
43
|
+
|
|
44
|
+
- `Auto`: prefer direct provider API when configured, otherwise use Portkey if the provider is gateway-only.
|
|
45
|
+
- `Prefer Direct`: use direct provider API when available; fall back to Portkey only when direct access is absent.
|
|
46
|
+
- `Prefer Portkey`: use the Portkey connection for that provider when available; fall back to direct only when no Portkey route exists.
|
|
47
|
+
|
|
48
|
+
The Gateway section owns account-level operations:
|
|
49
|
+
|
|
50
|
+
- connect/add a Portkey route
|
|
51
|
+
- sync all Portkey model catalogs
|
|
52
|
+
- apply Portkey to all providers with Portkey routes
|
|
53
|
+
- disable Portkey as the default by returning provider policies to `Auto`
|
|
54
|
+
- remove all Portkey gateway connections
|
|
55
|
+
|
|
56
|
+
The Model Catalog remains an inspection surface. It shows imported/discovered models, capabilities, pricing, source, and verification status, but the default access decision belongs on provider cards.
|
|
57
|
+
|
|
27
58
|
## `#models` Page Behavior
|
|
28
59
|
|
|
29
60
|
### Provider Cards
|
|
@@ -53,6 +84,32 @@ Each saved provider instance should display a compact connection chip:
|
|
|
53
84
|
|
|
54
85
|
The chip is metadata, not the provider type.
|
|
55
86
|
|
|
87
|
+
Each cloud provider card also exposes an **Access Route** control:
|
|
88
|
+
|
|
89
|
+
- `Use Portkey` when a Portkey route exists for that provider.
|
|
90
|
+
- `Prefer Direct` for direct-first routing.
|
|
91
|
+
- `Auto` for direct-first with gateway fallback.
|
|
92
|
+
- `Enable Portkey` when the provider has no Portkey route yet.
|
|
93
|
+
|
|
94
|
+
This gives users a discoverable per-provider control without forcing route choices across hundreds of model rows.
|
|
95
|
+
|
|
96
|
+
### Gateway Section
|
|
97
|
+
|
|
98
|
+
The Models page includes a first-class **Gateways** section above provider cards.
|
|
99
|
+
|
|
100
|
+
The Portkey gateway card displays:
|
|
101
|
+
|
|
102
|
+
- configured provider route count
|
|
103
|
+
- imported model count
|
|
104
|
+
- last successful sync or last sync error
|
|
105
|
+
- `Connect Portkey` / `Add Route`
|
|
106
|
+
- `Sync now`
|
|
107
|
+
- `Apply to all providers`
|
|
108
|
+
- `Disable as default`
|
|
109
|
+
- `Remove`
|
|
110
|
+
|
|
111
|
+
Portkey model discovery runs hourly in the Wall-E daemon, runs on startup after the daemon is listening, and can be triggered manually from `Sync now`. Sync must import raw Portkey model ids, including model ids not present in CTM's built-in supported-model catalog.
|
|
112
|
+
|
|
56
113
|
### Add Provider Dialog
|
|
57
114
|
|
|
58
115
|
The dialog should ask for upstream first, then connection:
|
|
@@ -72,6 +129,8 @@ Advanced custom headers can still exist, but common Portkey fields should not re
|
|
|
72
129
|
|
|
73
130
|
### Model List
|
|
74
131
|
|
|
132
|
+
The Model Catalog section is collapsed by default. Searching models expands the catalog so search results remain visible.
|
|
133
|
+
|
|
75
134
|
The model list should show duplicate model ids as separate routes only when necessary:
|
|
76
135
|
|
|
77
136
|
- If a model exists once, show `GPT-5.5` with a subtle `OpenAI · Direct` chip.
|
|
@@ -81,13 +140,23 @@ The model list should show duplicate model ids as separate routes only when nece
|
|
|
81
140
|
|
|
82
141
|
The saved value should prefer `model_registry.id`, not only the bare model id.
|
|
83
142
|
|
|
143
|
+
Gateway-discovered model rows use:
|
|
144
|
+
|
|
145
|
+
- `source = portkey`
|
|
146
|
+
- `gateway_type = portkey`
|
|
147
|
+
- `verification_status = verified` only when CTM's built-in catalog recognizes the model id; otherwise `unverified`
|
|
148
|
+
- `last_seen_at` from the latest Portkey sync
|
|
149
|
+
|
|
150
|
+
Catalog pruning must not delete gateway-discovered rows just because CTM's built-in catalog does not know the id yet.
|
|
151
|
+
|
|
84
152
|
## Conflict Rules
|
|
85
153
|
|
|
86
154
|
1. Exact `model_registry.id` wins.
|
|
87
|
-
2.
|
|
88
|
-
3.
|
|
89
|
-
4. If a bare model id maps to
|
|
90
|
-
5.
|
|
155
|
+
2. Explicit task/model defaults win over provider route policy.
|
|
156
|
+
3. Provider route policy resolves duplicate direct and Portkey rows for the same provider family.
|
|
157
|
+
4. If a bare model id maps to exactly one enabled route, it is safe to resolve.
|
|
158
|
+
5. If a bare model id maps to multiple enabled routes and provider route policy cannot disambiguate, return an ambiguity error that names the available routes.
|
|
159
|
+
6. Never select the newest provider row as a silent tie-breaker when direct and Portkey routes share the same model id.
|
|
91
160
|
|
|
92
161
|
## Implementation Phases
|
|
93
162
|
|
|
@@ -117,38 +186,20 @@ Document the UX and routing model, including the `#models` page behavior and dup
|
|
|
117
186
|
|
|
118
187
|
## Verification Results
|
|
119
188
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
- CTM: `https://localhost:3466/?token=...#models`
|
|
123
|
-
- Wall-E: `127.0.0.1:3467`
|
|
124
|
-
- Data dir: `/tmp/ctm-portkey-test`
|
|
125
|
-
- Mock Portkey gateway: `http://127.0.0.1:4577/v1`
|
|
126
|
-
|
|
127
|
-
The seeded real-life scenario used two OpenAI connections with the same model id:
|
|
128
|
-
|
|
129
|
-
- `openai-default:gpt-4o-mini` as direct OpenAI.
|
|
130
|
-
- `openai-portkey-prod:gpt-4o-mini` as OpenAI via Portkey.
|
|
131
|
-
|
|
132
|
-
The CTM `test-connection` route sent a live `GET /v1/models` request through Wall-E's OpenAI adapter to the mock gateway. The mock observed:
|
|
133
|
-
|
|
134
|
-
- `Authorization: Bearer unit-portkey-key`
|
|
135
|
-
- `x-portkey-config: cfg-prod`
|
|
136
|
-
- `x-portkey-provider: openai`
|
|
137
|
-
|
|
138
|
-
The browser verification captured desktop and mobile screenshots in `/tmp/portkey-models-desktop.png` and `/tmp/portkey-models-mobile.png`. It checked:
|
|
139
|
-
|
|
140
|
-
- Duplicate direct and Portkey OpenAI routes render as separate connections.
|
|
141
|
-
- Portkey route details show `config cfg-prod`.
|
|
142
|
-
- Header `+ Add Provider` keeps the Provider selector visible.
|
|
143
|
-
- Portkey mode exposes gateway fields and labels the key as `Portkey API Key`.
|
|
144
|
-
- Anthropic defaults to `https://api.portkey.ai`; OpenAI defaults to `https://api.portkey.ai/v1`.
|
|
145
|
-
- The Portkey provider-slug placeholder follows the selected provider.
|
|
146
|
-
- The Portkey dialog itself fits a 390px mobile viewport.
|
|
189
|
+
The implemented provider-level gateway path is covered by:
|
|
147
190
|
|
|
148
|
-
|
|
191
|
+
- Portkey sync tests that import unknown gateway model ids, mark CTM-known ids as `verified`, mark unknown ids as `unverified`, and prove catalog pruning preserves `source = portkey` rows.
|
|
192
|
+
- Provider route-policy tests that prefer Portkey or direct connections per provider and fall back when the requested route is unavailable.
|
|
193
|
+
- Chat routing tests that resolve duplicate direct/Portkey bare model ids using the provider policy instead of raising avoidable ambiguity.
|
|
194
|
+
- Model admin HTTP tests for gateway summaries and provider route policy updates.
|
|
195
|
+
- Static UI tests for the Gateway section, provider Access Route controls, and collapsed-by-default Model Catalog.
|
|
196
|
+
- Playwright render tests for the Models page gateway card, provider route buttons, collapsed catalog expansion, and provider policy POST behavior.
|
|
197
|
+
- Full CTM regression tests.
|
|
149
198
|
|
|
150
199
|
## References
|
|
151
200
|
|
|
152
201
|
- https://portkey.ai/docs/integrations/libraries/openai-compatible
|
|
202
|
+
- https://portkey.ai/docs/api-reference/inference-api/models/models
|
|
203
|
+
- https://portkey.ai/docs/product/ai-gateway/universal-api
|
|
153
204
|
- https://portkey.ai/docs/integrations/libraries/claude-code
|
|
154
205
|
- https://portkey.ai/docs/integrations/libraries/codex
|
package/template/package.json
CHANGED
|
@@ -1430,8 +1430,19 @@ function handleWalleApi(req, res, url) {
|
|
|
1430
1430
|
if (p === '/api/wall-e/alerts' && m === 'GET') {
|
|
1431
1431
|
try {
|
|
1432
1432
|
const { getServiceAlerts } = require('./skills/skill-planner');
|
|
1433
|
+
const { buildServiceHealth } = require('./lib/service-health');
|
|
1433
1434
|
const alerts = getServiceAlerts({ includeVersionUpdate: true });
|
|
1434
|
-
|
|
1435
|
+
let activeProvider = process.env.WALLE_PROVIDER || '';
|
|
1436
|
+
let activeModel = process.env.WALLE_MODEL || '';
|
|
1437
|
+
try {
|
|
1438
|
+
activeProvider = brain?.getKv?.('walle_provider') || activeProvider;
|
|
1439
|
+
activeModel = brain?.getKv?.('walle_model') || activeModel;
|
|
1440
|
+
} catch {}
|
|
1441
|
+
const summary = buildServiceHealth(alerts, {
|
|
1442
|
+
activeProvider,
|
|
1443
|
+
activeModel,
|
|
1444
|
+
});
|
|
1445
|
+
return jsonResponse(res, { alerts, summary }), true;
|
|
1435
1446
|
} catch (e) {
|
|
1436
1447
|
return jsonResponse(res, { alerts: [], error: e.message }), true;
|
|
1437
1448
|
}
|
|
@@ -2323,9 +2334,15 @@ function handleWalleApi(req, res, url) {
|
|
|
2323
2334
|
if (!hasApiKey && (walleProvider === 'ollama' || walleProvider === 'mlx')) hasApiKey = true;
|
|
2324
2335
|
|
|
2325
2336
|
let serviceAlerts = [];
|
|
2337
|
+
let serviceAlertSummary = null;
|
|
2326
2338
|
try {
|
|
2327
2339
|
const { getServiceAlerts } = require('./skills/skill-planner');
|
|
2340
|
+
const { buildServiceHealth } = require('./lib/service-health');
|
|
2328
2341
|
serviceAlerts = getServiceAlerts({ includeVersionUpdate: true });
|
|
2342
|
+
serviceAlertSummary = buildServiceHealth(serviceAlerts, {
|
|
2343
|
+
activeProvider: walleProvider,
|
|
2344
|
+
activeModel: walleModel,
|
|
2345
|
+
});
|
|
2329
2346
|
} catch {}
|
|
2330
2347
|
|
|
2331
2348
|
let slackSocketMode = null;
|
|
@@ -2350,6 +2367,7 @@ function handleWalleApi(req, res, url) {
|
|
|
2350
2367
|
walle_model: walleModel,
|
|
2351
2368
|
has_api_key: hasApiKey,
|
|
2352
2369
|
service_alerts: serviceAlerts,
|
|
2370
|
+
service_alert_summary: serviceAlertSummary,
|
|
2353
2371
|
slack_socket_mode: slackSocketMode,
|
|
2354
2372
|
}
|
|
2355
2373
|
}), true;
|
package/template/wall-e/brain.js
CHANGED
|
@@ -9,6 +9,7 @@ const { createWriteAuditLog } = require('./db/write-audit');
|
|
|
9
9
|
const { inferProviderFromModel, normalizeEvalProvider } = require('./eval/provider-normalizer');
|
|
10
10
|
const { capabilitiesForOllamaModel } = require('./llm/ollama');
|
|
11
11
|
const { findSupportedModel, supportedModelIdsForProvider } = require('./llm/supported-models');
|
|
12
|
+
const { isPortkeyProviderConfig } = require('./llm/portkey');
|
|
12
13
|
const _brainEvents = new EventEmitter();
|
|
13
14
|
_brainEvents.setMaxListeners(20);
|
|
14
15
|
let _stripNoise;
|
|
@@ -37,7 +38,7 @@ let currentDbPath = null;
|
|
|
37
38
|
let _daemonOwned = false; // When true, closeDb() is a no-op (daemon manages lifecycle)
|
|
38
39
|
|
|
39
40
|
// --- Schema versioning via PRAGMA user_version ---
|
|
40
|
-
const SCHEMA_VERSION =
|
|
41
|
+
const SCHEMA_VERSION = 15; // Bump on every migration addition
|
|
41
42
|
|
|
42
43
|
const MIGRATIONS = {
|
|
43
44
|
1: (d) => {
|
|
@@ -365,6 +366,19 @@ const MIGRATIONS = {
|
|
|
365
366
|
ON channel_message_events(delivery_id);
|
|
366
367
|
`);
|
|
367
368
|
},
|
|
369
|
+
15: (d) => {
|
|
370
|
+
const addCol = (table, col, type) => {
|
|
371
|
+
try { d.prepare(`SELECT ${col} FROM ${table} LIMIT 0`).run(); } catch (_) {
|
|
372
|
+
d.prepare(`ALTER TABLE ${table} ADD COLUMN ${col} ${type}`).run();
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
addCol('model_registry', 'source', "TEXT DEFAULT 'catalog'");
|
|
376
|
+
addCol('model_registry', 'verification_status', "TEXT DEFAULT 'verified'");
|
|
377
|
+
addCol('model_registry', 'last_seen_at', 'TEXT');
|
|
378
|
+
addCol('model_registry', 'gateway_type', 'TEXT');
|
|
379
|
+
d.prepare("UPDATE model_registry SET source = 'catalog' WHERE source IS NULL OR source = ''").run();
|
|
380
|
+
d.prepare("UPDATE model_registry SET verification_status = 'verified' WHERE verification_status IS NULL OR verification_status = ''").run();
|
|
381
|
+
},
|
|
368
382
|
};
|
|
369
383
|
|
|
370
384
|
// Schema invariants — columns/tables that MUST exist after the named migration.
|
|
@@ -382,6 +396,7 @@ const SCHEMA_INVARIANTS = [
|
|
|
382
396
|
{ migration: 9, table: 'agent_runner_evaluations', column: 'runner_id' },
|
|
383
397
|
{ migration: 10, table: 'eval_benchmark_runs', column: 'dataset_version' },
|
|
384
398
|
{ migration: 14, table: 'channel_message_events', column: 'channel' },
|
|
399
|
+
{ migration: 15, table: 'model_registry', column: 'source' },
|
|
385
400
|
];
|
|
386
401
|
|
|
387
402
|
function _columnExists(d, table, column) {
|
|
@@ -1071,6 +1086,10 @@ function createTables() {
|
|
|
1071
1086
|
speed_tier INTEGER DEFAULT 3,
|
|
1072
1087
|
enabled INTEGER DEFAULT 1,
|
|
1073
1088
|
is_fine_tuned INTEGER DEFAULT 0,
|
|
1089
|
+
source TEXT DEFAULT 'catalog',
|
|
1090
|
+
verification_status TEXT DEFAULT 'verified',
|
|
1091
|
+
last_seen_at TEXT,
|
|
1092
|
+
gateway_type TEXT,
|
|
1074
1093
|
UNIQUE(provider_id, model_id)
|
|
1075
1094
|
);
|
|
1076
1095
|
|
|
@@ -3104,6 +3123,90 @@ function pruneSlackInboundEvents(ttlMs = 48 * 60 * 60 * 1000) {
|
|
|
3104
3123
|
|
|
3105
3124
|
// -- Model Provider CRUD --
|
|
3106
3125
|
|
|
3126
|
+
const VALID_PROVIDER_ROUTE_POLICIES = new Set(['auto', 'direct', 'portkey']);
|
|
3127
|
+
|
|
3128
|
+
function _modelProviderRoutePolicyKey(type) {
|
|
3129
|
+
return `model_provider_route_policy:${String(type || '').trim().toLowerCase()}`;
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
function _modelProviderConnectionKind(row = {}) {
|
|
3133
|
+
if (row.auth_method && row.auth_method !== 'api_key') return row.auth_method;
|
|
3134
|
+
return isPortkeyProviderConfig({
|
|
3135
|
+
baseUrl: row.base_url || row.baseUrl,
|
|
3136
|
+
customHeaders: row.custom_headers || row.customHeaders,
|
|
3137
|
+
}) ? 'portkey' : 'direct';
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
function _modelProviderHasCredential(row = {}) {
|
|
3141
|
+
const type = row.type || row.provider_type;
|
|
3142
|
+
if (!row) return false;
|
|
3143
|
+
if (type === 'ollama' || type === 'mlx') return true;
|
|
3144
|
+
if (row.auth_method && row.auth_method !== 'api_key') return true;
|
|
3145
|
+
if (row.api_key_encrypted) return true;
|
|
3146
|
+
if (type === 'anthropic' && process.env.ANTHROPIC_API_KEY) return true;
|
|
3147
|
+
if (type === 'openai' && process.env.OPENAI_API_KEY) return true;
|
|
3148
|
+
if (type === 'google' && (process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY)) return true;
|
|
3149
|
+
if (type === 'deepseek' && process.env.DEEPSEEK_API_KEY) return true;
|
|
3150
|
+
if (type === 'moonshot' && process.env.MOONSHOT_API_KEY) return true;
|
|
3151
|
+
return false;
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
function _modelProviderRouteRank(row = {}, policy = 'auto') {
|
|
3155
|
+
const kind = _modelProviderConnectionKind(row);
|
|
3156
|
+
const isPortkey = kind === 'portkey';
|
|
3157
|
+
const hasCredential = _modelProviderHasCredential(row);
|
|
3158
|
+
let policyRank = 50;
|
|
3159
|
+
if (policy === 'portkey') policyRank = isPortkey ? 0 : 10;
|
|
3160
|
+
else if (policy === 'direct') policyRank = isPortkey ? 10 : 0;
|
|
3161
|
+
else policyRank = isPortkey ? 10 : 0;
|
|
3162
|
+
const credentialRank = hasCredential ? 0 : 20;
|
|
3163
|
+
const defaultRank = /-(default|auto)$/.test(String(row.id || '')) ? 0 : 2;
|
|
3164
|
+
return policyRank + credentialRank + defaultRank;
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3167
|
+
function sortModelProvidersByRoutePolicy(rows = [], policy = 'auto') {
|
|
3168
|
+
const normalizedPolicy = VALID_PROVIDER_ROUTE_POLICIES.has(policy) ? policy : 'auto';
|
|
3169
|
+
return [...(rows || [])].sort((a, b) => {
|
|
3170
|
+
const ar = _modelProviderRouteRank(a, normalizedPolicy);
|
|
3171
|
+
const br = _modelProviderRouteRank(b, normalizedPolicy);
|
|
3172
|
+
if (ar !== br) return ar - br;
|
|
3173
|
+
const au = Date.parse(a.updated_at || '') || 0;
|
|
3174
|
+
const bu = Date.parse(b.updated_at || '') || 0;
|
|
3175
|
+
if (bu !== au) return bu - au;
|
|
3176
|
+
return String(a.name || a.id || '').localeCompare(String(b.name || b.id || ''));
|
|
3177
|
+
});
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
function setProviderRoutePolicy({ type, policy }) {
|
|
3181
|
+
const providerType = String(type || '').trim().toLowerCase();
|
|
3182
|
+
const routePolicy = String(policy || 'auto').trim().toLowerCase();
|
|
3183
|
+
if (!providerType) throw new Error('Provider type required');
|
|
3184
|
+
if (!VALID_PROVIDER_ROUTE_POLICIES.has(routePolicy)) {
|
|
3185
|
+
throw new Error(`Invalid provider route policy: ${routePolicy}`);
|
|
3186
|
+
}
|
|
3187
|
+
setKv(_modelProviderRoutePolicyKey(providerType), routePolicy);
|
|
3188
|
+
return { type: providerType, policy: routePolicy };
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
function getProviderRoutePolicy(type) {
|
|
3192
|
+
const providerType = String(type || '').trim().toLowerCase();
|
|
3193
|
+
if (!providerType) return 'auto';
|
|
3194
|
+
const policy = getKv(_modelProviderRoutePolicyKey(providerType));
|
|
3195
|
+
return VALID_PROVIDER_ROUTE_POLICIES.has(policy) ? policy : 'auto';
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
function getPreferredModelProviderForType(type) {
|
|
3199
|
+
const providerType = String(type || '').trim().toLowerCase();
|
|
3200
|
+
if (!providerType) return null;
|
|
3201
|
+
const rows = getDb().prepare(`
|
|
3202
|
+
SELECT *
|
|
3203
|
+
FROM model_providers
|
|
3204
|
+
WHERE type = ? AND enabled = 1
|
|
3205
|
+
`).all(providerType);
|
|
3206
|
+
if (!rows.length) return null;
|
|
3207
|
+
return sortModelProvidersByRoutePolicy(rows, getProviderRoutePolicy(providerType))[0] || null;
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3107
3210
|
function upsertModelProvider({ id, name, type, baseUrl, apiKeyEncrypted, customHeaders, enabled }) {
|
|
3108
3211
|
if (!id || !name || !type) throw new Error('Provider requires id, name, type');
|
|
3109
3212
|
getDb().prepare(`
|
|
@@ -3294,14 +3397,31 @@ function disableModelProviderByType(type) {
|
|
|
3294
3397
|
|
|
3295
3398
|
// -- Model Registry CRUD --
|
|
3296
3399
|
|
|
3297
|
-
function upsertModelRegistryEntry({
|
|
3400
|
+
function upsertModelRegistryEntry({
|
|
3401
|
+
id,
|
|
3402
|
+
providerId,
|
|
3403
|
+
modelId,
|
|
3404
|
+
displayName,
|
|
3405
|
+
capabilities,
|
|
3406
|
+
costPer1mInput,
|
|
3407
|
+
costPer1mOutput,
|
|
3408
|
+
maxContextTokens,
|
|
3409
|
+
maxOutputTokens,
|
|
3410
|
+
speedTier,
|
|
3411
|
+
enabled,
|
|
3412
|
+
isFineTuned,
|
|
3413
|
+
source,
|
|
3414
|
+
verificationStatus,
|
|
3415
|
+
lastSeenAt,
|
|
3416
|
+
gatewayType,
|
|
3417
|
+
}) {
|
|
3298
3418
|
if (!id || !providerId || !modelId || !displayName) throw new Error('Registry entry requires id, providerId, modelId, displayName');
|
|
3299
3419
|
const caps = (Array.isArray(capabilities) || (capabilities && typeof capabilities === 'object'))
|
|
3300
3420
|
? JSON.stringify(capabilities)
|
|
3301
3421
|
: (capabilities || '[]');
|
|
3302
3422
|
getDb().prepare(`
|
|
3303
|
-
INSERT INTO model_registry (id, provider_id, model_id, display_name, capabilities, cost_per_1m_input, cost_per_1m_output, max_context_tokens, max_output_tokens, speed_tier, enabled, is_fine_tuned)
|
|
3304
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3423
|
+
INSERT INTO model_registry (id, provider_id, model_id, display_name, capabilities, cost_per_1m_input, cost_per_1m_output, max_context_tokens, max_output_tokens, speed_tier, enabled, is_fine_tuned, source, verification_status, last_seen_at, gateway_type)
|
|
3424
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3305
3425
|
ON CONFLICT(id) DO UPDATE SET
|
|
3306
3426
|
provider_id = excluded.provider_id,
|
|
3307
3427
|
model_id = excluded.model_id,
|
|
@@ -3313,8 +3433,29 @@ function upsertModelRegistryEntry({ id, providerId, modelId, displayName, capabi
|
|
|
3313
3433
|
max_output_tokens = excluded.max_output_tokens,
|
|
3314
3434
|
speed_tier = excluded.speed_tier,
|
|
3315
3435
|
enabled = excluded.enabled,
|
|
3316
|
-
is_fine_tuned = excluded.is_fine_tuned
|
|
3317
|
-
|
|
3436
|
+
is_fine_tuned = excluded.is_fine_tuned,
|
|
3437
|
+
source = COALESCE(excluded.source, model_registry.source, 'catalog'),
|
|
3438
|
+
verification_status = COALESCE(excluded.verification_status, model_registry.verification_status, 'verified'),
|
|
3439
|
+
last_seen_at = COALESCE(excluded.last_seen_at, model_registry.last_seen_at),
|
|
3440
|
+
gateway_type = COALESCE(excluded.gateway_type, model_registry.gateway_type)
|
|
3441
|
+
`).run(
|
|
3442
|
+
id,
|
|
3443
|
+
providerId,
|
|
3444
|
+
modelId,
|
|
3445
|
+
displayName,
|
|
3446
|
+
caps,
|
|
3447
|
+
costPer1mInput || null,
|
|
3448
|
+
costPer1mOutput || null,
|
|
3449
|
+
maxContextTokens || null,
|
|
3450
|
+
maxOutputTokens || null,
|
|
3451
|
+
speedTier || 3,
|
|
3452
|
+
enabled !== undefined ? (enabled ? 1 : 0) : 1,
|
|
3453
|
+
isFineTuned ? 1 : 0,
|
|
3454
|
+
source || null,
|
|
3455
|
+
verificationStatus || null,
|
|
3456
|
+
lastSeenAt || null,
|
|
3457
|
+
gatewayType || null,
|
|
3458
|
+
);
|
|
3318
3459
|
}
|
|
3319
3460
|
|
|
3320
3461
|
function getModelRegistryEntry(id) {
|
|
@@ -3684,6 +3825,7 @@ function pruneUnsupportedCloudModelRegistryRowsForDb(d, { dryRun = false } = {})
|
|
|
3684
3825
|
JOIN model_providers mp ON mr.provider_id = mp.id
|
|
3685
3826
|
WHERE mp.type IN ('anthropic', 'openai', 'google', 'deepseek', 'moonshot')
|
|
3686
3827
|
AND COALESCE(mr.is_fine_tuned, 0) = 0
|
|
3828
|
+
AND COALESCE(mr.source, 'catalog') IN ('catalog', 'builtin')
|
|
3687
3829
|
`).all();
|
|
3688
3830
|
const stale = rows.filter((row) => {
|
|
3689
3831
|
const allowed = allowedByType.get(row.provider_type);
|
|
@@ -4626,6 +4768,10 @@ module.exports = {
|
|
|
4626
4768
|
listModelProviders,
|
|
4627
4769
|
listEnabledProviders,
|
|
4628
4770
|
getProviderByType,
|
|
4771
|
+
setProviderRoutePolicy,
|
|
4772
|
+
getProviderRoutePolicy,
|
|
4773
|
+
getPreferredModelProviderForType,
|
|
4774
|
+
sortModelProvidersByRoutePolicy,
|
|
4629
4775
|
setProviderAuthMethod,
|
|
4630
4776
|
getProviderAuthMethod,
|
|
4631
4777
|
saveSetupProvider,
|
package/template/wall-e/chat.js
CHANGED
|
@@ -112,6 +112,32 @@ function looksLikePrematureToolFollowupReply(text) {
|
|
|
112
112
|
return concise && progressCue && (sequenceCue || executionVerb);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
function _chatToolCallHistoryForActionGuard(toolCalls = []) {
|
|
116
|
+
return (toolCalls || [])
|
|
117
|
+
.map((call) => {
|
|
118
|
+
const name = call?.name || call?.tool || '';
|
|
119
|
+
if (!name) return null;
|
|
120
|
+
let inputHash = '';
|
|
121
|
+
try {
|
|
122
|
+
inputHash = JSON.stringify(call.input || call.args || {}).slice(0, 500);
|
|
123
|
+
} catch {
|
|
124
|
+
inputHash = String(call.input || call.args || '').slice(0, 500);
|
|
125
|
+
}
|
|
126
|
+
return { name, inputHash };
|
|
127
|
+
})
|
|
128
|
+
.filter(Boolean);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function _chatNoActionContinuation(args) {
|
|
132
|
+
try {
|
|
133
|
+
const { getNoActionContinuation } = require('./coding-orchestrator');
|
|
134
|
+
return getNoActionContinuation(args);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.warn('[chat] Action completion guard unavailable:', err.message);
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
115
141
|
function _hasExplicitWeatherLocation(message) {
|
|
116
142
|
const text = String(message || '').trim();
|
|
117
143
|
if (!text) return false;
|
|
@@ -734,6 +760,20 @@ function _ambiguousModelRoute(input, rows, explicitProvider) {
|
|
|
734
760
|
};
|
|
735
761
|
}
|
|
736
762
|
|
|
763
|
+
function _preferredRouteRowForBareModel(rows = [], explicitProvider) {
|
|
764
|
+
if (!rows.length) return null;
|
|
765
|
+
const type = explicitProvider || rows[0].provider_type;
|
|
766
|
+
const policy = (type && typeof brain.getProviderRoutePolicy === 'function')
|
|
767
|
+
? brain.getProviderRoutePolicy(type)
|
|
768
|
+
: 'auto';
|
|
769
|
+
if (typeof brain.sortModelProvidersByRoutePolicy !== 'function') return null;
|
|
770
|
+
const sorted = brain.sortModelProvidersByRoutePolicy(
|
|
771
|
+
rows.map((row) => ({ ...row, type: row.provider_type || row.type })),
|
|
772
|
+
policy,
|
|
773
|
+
);
|
|
774
|
+
return sorted[0] || null;
|
|
775
|
+
}
|
|
776
|
+
|
|
737
777
|
function _createChatProvider(type, config = {}, opts = {}) {
|
|
738
778
|
if (typeof opts.providerFactory === 'function') return opts.providerFactory(type, config);
|
|
739
779
|
return createClient(type, config);
|
|
@@ -1462,6 +1502,8 @@ function resolveModelSelection(model, explicitProvider) {
|
|
|
1462
1502
|
const defaultRegistryId = (brain.getKv?.('walle_model_registry_id') || (explicitProvider ? brain.getKv?.(`walle_model_registry_${explicitProvider}`) : null) || '').trim();
|
|
1463
1503
|
const defaultRow = defaultRegistryId ? rows.find((row) => row.id === defaultRegistryId) : null;
|
|
1464
1504
|
if (defaultRow) return _registryRouteFromRow(defaultRow, raw, explicitProvider);
|
|
1505
|
+
const preferredRow = _preferredRouteRowForBareModel(rows, explicitProvider);
|
|
1506
|
+
if (preferredRow) return _registryRouteFromRow(preferredRow, raw, explicitProvider);
|
|
1465
1507
|
return _ambiguousModelRoute(raw, rows, explicitProvider);
|
|
1466
1508
|
}
|
|
1467
1509
|
} catch {}
|
|
@@ -2868,6 +2910,7 @@ async function chat(message, opts = {}) {
|
|
|
2868
2910
|
let forceFinalNoTools = false;
|
|
2869
2911
|
let toolLimitNoticeSent = false;
|
|
2870
2912
|
let postToolProgressContinuationCount = 0;
|
|
2913
|
+
let actionCompletionContinuationCount = 0;
|
|
2871
2914
|
|
|
2872
2915
|
// Adaptive limits based on intent classification
|
|
2873
2916
|
const INTENT_LIMITS = {
|
|
@@ -3266,6 +3309,48 @@ async function chat(message, opts = {}) {
|
|
|
3266
3309
|
console.log('[chat] Premature post-tool progress text — continuing tool loop');
|
|
3267
3310
|
continue;
|
|
3268
3311
|
}
|
|
3312
|
+
|
|
3313
|
+
if ((wallECodingMode || isCodingActionRequest()) && !forceFinalNoTools) {
|
|
3314
|
+
let hasTurnBudgetForActionContinuation = turn + 1 < allowedTurns;
|
|
3315
|
+
if (!hasTurnBudgetForActionContinuation) {
|
|
3316
|
+
hasTurnBudgetForActionContinuation = extendCodingTurnBudget('action completion guard');
|
|
3317
|
+
}
|
|
3318
|
+
const toolsAvailableForActionContinuation = Array.isArray(toolsForTurn)
|
|
3319
|
+
&& toolsForTurn.length > 0
|
|
3320
|
+
&& toolCallCount < MAX_TOOL_CALLS
|
|
3321
|
+
&& hasTurnBudgetForActionContinuation;
|
|
3322
|
+
const actionContinuation = _chatNoActionContinuation({
|
|
3323
|
+
prompt: routingMessage || message,
|
|
3324
|
+
content: finalText,
|
|
3325
|
+
toolCallHistory: _chatToolCallHistoryForActionGuard(allToolCalls),
|
|
3326
|
+
toolsAvailable: toolsAvailableForActionContinuation,
|
|
3327
|
+
nudges: actionCompletionContinuationCount,
|
|
3328
|
+
maxNudges: wallECodingMode ? 3 : 1,
|
|
3329
|
+
cwd: effectiveCwd || opts.cwd || process.cwd(),
|
|
3330
|
+
});
|
|
3331
|
+
if (actionContinuation?.action === 'continue') {
|
|
3332
|
+
actionCompletionContinuationCount += 1;
|
|
3333
|
+
const assistantHistoryContent = [];
|
|
3334
|
+
if (response.reasoningContent && typeof response.reasoningContent === 'string') {
|
|
3335
|
+
assistantHistoryContent.push({ type: 'reasoning', text: response.reasoningContent });
|
|
3336
|
+
}
|
|
3337
|
+
if (finalText) assistantHistoryContent.push({ type: 'text', text: finalText });
|
|
3338
|
+
messages.push({
|
|
3339
|
+
role: 'assistant',
|
|
3340
|
+
content: assistantHistoryContent.length > 0 ? assistantHistoryContent : finalText,
|
|
3341
|
+
});
|
|
3342
|
+
messages.push({
|
|
3343
|
+
role: 'user',
|
|
3344
|
+
content: `${actionContinuation.message}\n` +
|
|
3345
|
+
'If the target project is outside the current working directory, do not run broad filesystem scans. Use start_coding with cwd set to the explicit target project directory, or use project-scoped file tools against that directory. For website/UI/UX requests, finish only after concrete edits and verification evidence.',
|
|
3346
|
+
});
|
|
3347
|
+
console.log('[chat] Action completion guard — continuing tool loop:', actionContinuation.reason);
|
|
3348
|
+
continue;
|
|
3349
|
+
}
|
|
3350
|
+
if (actionContinuation?.action === 'fail') {
|
|
3351
|
+
finalText = `I could not complete the requested code change: ${actionContinuation.reason}`;
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3269
3354
|
finalResponseMeta = {
|
|
3270
3355
|
model: response.model || selectedModel,
|
|
3271
3356
|
provider: response.provider || usedProvider || targetProviderType,
|