@co0ontty/wand 4.83.1 → 4.84.0
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 +4 -0
- package/dist/agent-dispatch.js +5 -1
- package/dist/ai-team-availability.d.ts +1 -1
- package/dist/ai-team-prompts.js +13 -4
- package/dist/ai-team-runner.d.ts +80 -5
- package/dist/ai-team-runner.js +332 -55
- package/dist/ai-team-types.d.ts +92 -5
- package/dist/ai-team-types.js +9 -2
- package/dist/build-info.json +3 -3
- package/dist/cli-api.d.ts +1 -0
- package/dist/cli-api.js +3 -0
- package/dist/cli.js +18 -1
- package/dist/commit-task-archive.d.ts +3 -1
- package/dist/commit-task-archive.js +21 -10
- package/dist/config.d.ts +1 -1
- package/dist/config.js +73 -0
- package/dist/conversation-mentions.d.ts +16 -0
- package/dist/conversation-mentions.js +32 -0
- package/dist/conversation-service.d.ts +90 -0
- package/dist/conversation-service.js +759 -0
- package/dist/conversation-session-preview.d.ts +9 -0
- package/dist/conversation-session-preview.js +47 -0
- package/dist/conversation-task-preview.d.ts +5 -0
- package/dist/conversation-task-preview.js +8 -0
- package/dist/conversation-types.d.ts +80 -0
- package/dist/conversation-types.js +7 -0
- package/dist/core-extension-host.d.ts +27 -0
- package/dist/core-extension-host.js +138 -0
- package/dist/core-runner.d.ts +86 -0
- package/dist/core-runner.js +757 -0
- package/dist/core-status-cli.d.ts +9 -0
- package/dist/core-status-cli.js +280 -0
- package/dist/core-turn-tracker.d.ts +12 -0
- package/dist/core-turn-tracker.js +36 -0
- package/dist/decision-runner.d.ts +6 -0
- package/dist/decision-runner.js +3 -1
- package/dist/decision-tool.d.ts +2 -0
- package/dist/decision-tool.js +49 -22
- package/dist/env-utils.d.ts +7 -3
- package/dist/env-utils.js +30 -5
- package/dist/git-quick-commit.d.ts +5 -0
- package/dist/git-quick-commit.js +9 -3
- package/dist/git-utils.js +2 -1
- package/dist/harness-engine.d.ts +110 -0
- package/dist/harness-engine.js +331 -0
- package/dist/mission-diff.js +2 -1
- package/dist/model-auto-assign.d.ts +50 -0
- package/dist/model-auto-assign.js +163 -0
- package/dist/model-group-runner.d.ts +4 -0
- package/dist/model-group-runner.js +72 -0
- package/dist/model-groups.d.ts +38 -0
- package/dist/model-groups.js +138 -0
- package/dist/models.d.ts +12 -0
- package/dist/models.js +102 -8
- package/dist/npm-update-utils.js +2 -1
- package/dist/openrouter-free-cli.d.ts +25 -0
- package/dist/openrouter-free-cli.js +109 -0
- package/dist/openrouter-free-models.d.ts +85 -0
- package/dist/openrouter-free-models.js +458 -0
- package/dist/openrouter-free-selection.d.ts +5 -0
- package/dist/openrouter-free-selection.js +7 -0
- package/dist/path-repair.d.ts +10 -0
- package/dist/path-repair.js +50 -5
- package/dist/pi-auto-resources.d.ts +12 -0
- package/dist/pi-auto-resources.js +151 -0
- package/dist/pi-execution-types.d.ts +48 -0
- package/dist/pi-execution-types.js +1 -0
- package/dist/pi-execution.d.ts +9 -0
- package/dist/pi-execution.js +187 -0
- package/dist/pi-extension-resources.d.ts +13 -0
- package/dist/pi-extension-resources.js +71 -0
- package/dist/pi-openrouter-free-bridge.d.ts +2 -0
- package/dist/pi-openrouter-free-bridge.js +80 -0
- package/dist/pi-resource-bridge.d.ts +2 -0
- package/dist/pi-resource-bridge.js +73 -0
- package/dist/pi-resource-catalog.d.ts +18 -0
- package/dist/pi-resource-catalog.js +82 -0
- package/dist/pi-resource-recommendation.d.ts +29 -0
- package/dist/pi-resource-recommendation.js +169 -0
- package/dist/pi-resource-run.d.ts +8 -0
- package/dist/pi-resource-run.js +28 -0
- package/dist/pi-session-settings.d.ts +157 -0
- package/dist/pi-session-settings.js +166 -0
- package/dist/platform-action-catalog.d.ts +24 -0
- package/dist/platform-action-catalog.js +141 -0
- package/dist/platform-action-service.d.ts +37 -0
- package/dist/platform-action-service.js +138 -0
- package/dist/platform-action-types.d.ts +125 -0
- package/dist/platform-action-types.js +7 -0
- package/dist/process-manager.d.ts +21 -1
- package/dist/process-manager.js +168 -15
- package/dist/provider-catalog.js +1 -1
- package/dist/provider-cli-updater.js +29 -8
- package/dist/provider-history-scanner.d.ts +9 -0
- package/dist/provider-history-scanner.js +107 -1
- package/dist/retention.d.ts +19 -7
- package/dist/retention.js +171 -28
- package/dist/server-ai-team-routes.js +6 -2
- package/dist/server-conversation-routes.d.ts +5 -0
- package/dist/server-conversation-routes.js +91 -0
- package/dist/server-employee-routes.d.ts +2 -0
- package/dist/server-employee-routes.js +85 -11
- package/dist/server-pi-recommendation-routes.d.ts +13 -0
- package/dist/server-pi-recommendation-routes.js +67 -0
- package/dist/server-resume-routes.js +1 -1
- package/dist/server-session-routes.d.ts +4 -3
- package/dist/server-session-routes.js +182 -37
- package/dist/server-settings-routes.d.ts +5 -0
- package/dist/server-settings-routes.js +46 -0
- package/dist/server-task-routes.js +49 -4
- package/dist/server-team-dispatch-routes.d.ts +19 -0
- package/dist/server-team-dispatch-routes.js +71 -0
- package/dist/server-update-routes.d.ts +1 -1
- package/dist/server-update-routes.js +10 -5
- package/dist/server-workspace-routes.js +44 -24
- package/dist/server.d.ts +1 -0
- package/dist/server.js +148 -30
- package/dist/session-ai-context.d.ts +5 -1
- package/dist/session-ai-context.js +9 -4
- package/dist/session-registry.d.ts +4 -0
- package/dist/session-registry.js +47 -13
- package/dist/session-transport.js +8 -1
- package/dist/settings-web-access.d.ts +15 -0
- package/dist/settings-web-access.js +37 -0
- package/dist/silicon-employee-defaults.d.ts +29 -0
- package/dist/silicon-employee-defaults.js +41 -0
- package/dist/silicon-employee-dispatch.d.ts +5 -1
- package/dist/silicon-employee-dispatch.js +10 -4
- package/dist/silicon-employee-draft.d.ts +21 -4
- package/dist/silicon-employee-draft.js +103 -16
- package/dist/storage.d.ts +73 -2
- package/dist/storage.js +362 -26
- package/dist/structured-claude-protocol.js +1 -1
- package/dist/structured-client-protocol.js +11 -33
- package/dist/structured-content.d.ts +23 -0
- package/dist/structured-content.js +72 -0
- package/dist/structured-failure.d.ts +16 -0
- package/dist/structured-failure.js +36 -0
- package/dist/structured-pi-adapter.d.ts +8 -3
- package/dist/structured-pi-adapter.js +131 -30
- package/dist/structured-provider-common.d.ts +6 -0
- package/dist/structured-provider-common.js +23 -1
- package/dist/structured-runner.d.ts +45 -0
- package/dist/structured-session-manager.d.ts +78 -4
- package/dist/structured-session-manager.js +513 -129
- package/dist/structured-thinking.d.ts +15 -0
- package/dist/structured-thinking.js +46 -0
- package/dist/subagent-dispatch.d.ts +56 -0
- package/dist/subagent-dispatch.js +102 -0
- package/dist/system-employee.d.ts +4 -3
- package/dist/system-employee.js +7 -4
- package/dist/task-retention.d.ts +23 -0
- package/dist/task-retention.js +69 -0
- package/dist/task-types.d.ts +4 -0
- package/dist/task-types.js +3 -0
- package/dist/team-dispatch.d.ts +101 -0
- package/dist/team-dispatch.js +269 -0
- package/dist/tool-preview.js +7 -1
- package/dist/tui/commands.js +50 -1
- package/dist/tui/ipc-protocol.d.ts +1 -1
- package/dist/tui/ipc-server.d.ts +4 -0
- package/dist/tui/ipc-server.js +23 -0
- package/dist/turn-heartbeat.d.ts +35 -0
- package/dist/turn-heartbeat.js +64 -0
- package/dist/types.d.ts +147 -1
- package/dist/user-profile.d.ts +33 -0
- package/dist/user-profile.js +70 -0
- package/dist/web-ui/chat-message-meta.d.ts +11 -0
- package/dist/web-ui/chat-message-meta.js +47 -0
- package/dist/web-ui/content/ai-teams.js +13 -13
- package/dist/web-ui/content/scripts.js +795 -95
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +1 -6808
- package/dist/web-ui/content/vendor/qrcode/qrcode.bundle.js +1 -1
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +5 -5
- package/dist/web-ui/index.d.ts +1 -1
- package/dist/web-ui/index.js +3 -3
- package/dist/web-ui/markdown.d.ts +2 -0
- package/dist/web-ui/markdown.js +111 -0
- package/dist/web-ui/page.d.ts +4 -0
- package/dist/web-ui/page.js +8 -0
- package/dist/web-ui/react/local-preview/controller.d.ts +30 -0
- package/dist/web-ui/react/local-preview/controller.js +159 -0
- package/dist/web-ui/running-activity.d.ts +63 -0
- package/dist/web-ui/running-activity.js +134 -0
- package/dist/web-ui/styles.js +2 -11
- package/package.json +7 -2
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Model, Api } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { WandStorage } from "./storage.js";
|
|
3
|
+
import type { ClaudeModelInfo } from "./types.js";
|
|
4
|
+
export { OPENROUTER_FREE_PROVIDER, OPENROUTER_FREE_GROUP, OPENROUTER_FREE_SELECTOR, isOpenRouterFreeSelector } from "./openrouter-free-selection.js";
|
|
5
|
+
export declare const OPENROUTER_REFRESH_MS: number;
|
|
6
|
+
/** Same zero-price routing constraint for validation and actual core calls. */
|
|
7
|
+
export declare const OPENROUTER_FREE_ROUTING: {
|
|
8
|
+
require_parameters: boolean;
|
|
9
|
+
max_price: {
|
|
10
|
+
prompt: number;
|
|
11
|
+
completion: number;
|
|
12
|
+
request: number;
|
|
13
|
+
image: number;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export interface OpenRouterStatus {
|
|
17
|
+
configured: boolean;
|
|
18
|
+
group: string;
|
|
19
|
+
modelCount: number;
|
|
20
|
+
lastCheckedAt: string | null;
|
|
21
|
+
lastSyncedAt: string | null;
|
|
22
|
+
lastError: string | null;
|
|
23
|
+
refreshIntervalHours: number;
|
|
24
|
+
candidateCount: number;
|
|
25
|
+
rejectedCount: number;
|
|
26
|
+
}
|
|
27
|
+
/** Only zero-priced language models with text input/output and tool support can serve the agent. */
|
|
28
|
+
export declare function parseOpenRouterFreeModels(body: unknown): Model<Api>[];
|
|
29
|
+
export interface FreeModelRequirements {
|
|
30
|
+
preferReasoning?: boolean;
|
|
31
|
+
/** A custom group must never substitute a free model from outside its membership. */
|
|
32
|
+
allowedSelectors?: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
export type OpenRouterFreeStorage = Pick<WandStorage, "getConnectorToken" | "getConfigValue" | "setConfigValue" | "getPreference" | "saveConnector" | "deleteConnector">;
|
|
35
|
+
/** Server-owned credentials and last successful catalog; never writes Pi's auth/models files. */
|
|
36
|
+
export declare class OpenRouterFreeModelsService {
|
|
37
|
+
private readonly storage;
|
|
38
|
+
private readonly fetchImpl;
|
|
39
|
+
private readonly now;
|
|
40
|
+
private models;
|
|
41
|
+
private lastCheckedAt;
|
|
42
|
+
private lastSyncedAt;
|
|
43
|
+
private error;
|
|
44
|
+
private candidateCount;
|
|
45
|
+
private rejectedCount;
|
|
46
|
+
private revision;
|
|
47
|
+
private catalogSequence;
|
|
48
|
+
private committedSequence;
|
|
49
|
+
private credentialController;
|
|
50
|
+
private pending;
|
|
51
|
+
private controller;
|
|
52
|
+
private timer;
|
|
53
|
+
private disposed;
|
|
54
|
+
private listeners;
|
|
55
|
+
constructor(storage: OpenRouterFreeStorage, fetchImpl?: typeof fetch, now?: () => Date);
|
|
56
|
+
private key;
|
|
57
|
+
status(): OpenRouterStatus;
|
|
58
|
+
catalog(): ClaudeModelInfo[];
|
|
59
|
+
/** Concrete verified members for the group editor; no key or provider payload is exposed. */
|
|
60
|
+
members(): ClaudeModelInfo[];
|
|
61
|
+
private orderedModels;
|
|
62
|
+
resolve(selector: string | null | undefined, requirements?: FreeModelRequirements): {
|
|
63
|
+
model: Model<Api>;
|
|
64
|
+
apiKey: string;
|
|
65
|
+
} | null;
|
|
66
|
+
onChanged(listener: () => void): () => void;
|
|
67
|
+
private notify;
|
|
68
|
+
saveKey(value: unknown): Promise<OpenRouterStatus>;
|
|
69
|
+
clearKey(): OpenRouterStatus;
|
|
70
|
+
refresh(): Promise<OpenRouterStatus>;
|
|
71
|
+
private performRefresh;
|
|
72
|
+
private fetchCandidates;
|
|
73
|
+
private publish;
|
|
74
|
+
/** Fresh pricing is mandatory for each inference, including tool-loop continuations. */
|
|
75
|
+
resolveForCall(selector: string, signal?: AbortSignal, requirements?: FreeModelRequirements): Promise<{
|
|
76
|
+
model: Model<Api>;
|
|
77
|
+
apiKey: string;
|
|
78
|
+
}>;
|
|
79
|
+
/** Every discovered model must answer a real message before it becomes selectable. */
|
|
80
|
+
private validateModels;
|
|
81
|
+
private probeModel;
|
|
82
|
+
private closedStatus;
|
|
83
|
+
start(): void;
|
|
84
|
+
dispose(): void;
|
|
85
|
+
}
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { freeModelOrder, normalizeModelGroups, orderModelIds } from "./model-groups.js";
|
|
2
|
+
import { OPENROUTER_FREE_PROVIDER, OPENROUTER_FREE_GROUP, OPENROUTER_FREE_SELECTOR, isOpenRouterFreeSelector } from "./openrouter-free-selection.js";
|
|
3
|
+
export { OPENROUTER_FREE_PROVIDER, OPENROUTER_FREE_GROUP, OPENROUTER_FREE_SELECTOR, isOpenRouterFreeSelector } from "./openrouter-free-selection.js";
|
|
4
|
+
export const OPENROUTER_REFRESH_MS = 6 * 60 * 60 * 1000;
|
|
5
|
+
const CACHE_KEY = "openrouter-free-models-v1";
|
|
6
|
+
const MODEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,100}$/;
|
|
7
|
+
const MAX_BYTES = 4 * 1024 * 1024;
|
|
8
|
+
const PROBE_TIMEOUT_MS = 20000;
|
|
9
|
+
const PROBE_CONCURRENCY = 2;
|
|
10
|
+
const PROBE_VERSION = 2;
|
|
11
|
+
/** Same zero-price routing constraint for validation and actual core calls. */
|
|
12
|
+
export const OPENROUTER_FREE_ROUTING = {
|
|
13
|
+
require_parameters: true,
|
|
14
|
+
max_price: { prompt: 0, completion: 0, request: 0, image: 0 },
|
|
15
|
+
};
|
|
16
|
+
async function readJsonResponse(response, maxBytes) {
|
|
17
|
+
if (Number(response.headers.get("content-length")) > maxBytes) {
|
|
18
|
+
throw new OpenRouterSyncError("OpenRouter 返回内容过大。");
|
|
19
|
+
}
|
|
20
|
+
const reader = response.body?.getReader();
|
|
21
|
+
if (!reader)
|
|
22
|
+
throw new OpenRouterSyncError("OpenRouter 返回了空内容。");
|
|
23
|
+
const chunks = [];
|
|
24
|
+
let bytes = 0;
|
|
25
|
+
try {
|
|
26
|
+
while (true) {
|
|
27
|
+
const { done, value } = await reader.read();
|
|
28
|
+
if (done)
|
|
29
|
+
break;
|
|
30
|
+
bytes += value.byteLength;
|
|
31
|
+
if (bytes > maxBytes)
|
|
32
|
+
throw new OpenRouterSyncError("OpenRouter 返回内容过大。");
|
|
33
|
+
chunks.push(value);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
await reader.cancel().catch(() => { });
|
|
38
|
+
}
|
|
39
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
40
|
+
}
|
|
41
|
+
class OpenRouterSyncError extends Error {
|
|
42
|
+
}
|
|
43
|
+
function record(value) {
|
|
44
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
45
|
+
}
|
|
46
|
+
/** Only zero-priced language models with text input/output and tool support can serve the agent. */
|
|
47
|
+
export function parseOpenRouterFreeModels(body) {
|
|
48
|
+
const rows = record(body).data;
|
|
49
|
+
if (!Array.isArray(rows))
|
|
50
|
+
throw new OpenRouterSyncError("OpenRouter 返回的模型目录格式无效。");
|
|
51
|
+
const models = new Map();
|
|
52
|
+
for (const entry of rows.slice(0, 4000)) {
|
|
53
|
+
const row = record(entry);
|
|
54
|
+
const pricing = record(row.pricing);
|
|
55
|
+
const zero = (value) => (typeof value === "number" || (typeof value === "string" && value.trim() !== ""))
|
|
56
|
+
&& Number.isFinite(Number(value)) && Number(value) === 0;
|
|
57
|
+
if (!zero(pricing.prompt) || !zero(pricing.completion)
|
|
58
|
+
|| !Object.values(pricing).every(zero))
|
|
59
|
+
continue;
|
|
60
|
+
const architecture = record(row.architecture);
|
|
61
|
+
const inputs = architecture.input_modalities;
|
|
62
|
+
const outputs = architecture.output_modalities;
|
|
63
|
+
// Understanding images is allowed; generating images, video, audio, or unknown outputs is not.
|
|
64
|
+
if (!Array.isArray(inputs) || !inputs.includes("text")
|
|
65
|
+
|| !Array.isArray(outputs) || outputs.length === 0
|
|
66
|
+
|| !outputs.every((modality) => modality === "text"))
|
|
67
|
+
continue;
|
|
68
|
+
if (!Array.isArray(row.supported_parameters) || !row.supported_parameters.includes("tools"))
|
|
69
|
+
continue;
|
|
70
|
+
const id = typeof row.id === "string" ? row.id.trim() : "";
|
|
71
|
+
if (!MODEL_PATTERN.test(id))
|
|
72
|
+
continue;
|
|
73
|
+
const contextWindow = Number(row.context_length);
|
|
74
|
+
if (!Number.isSafeInteger(contextWindow) || contextWindow < 1024)
|
|
75
|
+
continue;
|
|
76
|
+
const reportedMax = Number(record(row.top_provider).max_completion_tokens);
|
|
77
|
+
const maxTokens = Number.isSafeInteger(reportedMax) && reportedMax > 0
|
|
78
|
+
? Math.min(reportedMax, contextWindow) : Math.min(8192, contextWindow);
|
|
79
|
+
models.set(id, {
|
|
80
|
+
id,
|
|
81
|
+
name: typeof row.name === "string" ? row.name.slice(0, 160) : id,
|
|
82
|
+
provider: OPENROUTER_FREE_PROVIDER,
|
|
83
|
+
api: "openai-completions",
|
|
84
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
85
|
+
reasoning: row.supported_parameters.includes("reasoning"),
|
|
86
|
+
compat: { thinkingFormat: "openrouter" },
|
|
87
|
+
input: Array.isArray(architecture.input_modalities) && architecture.input_modalities.includes("image")
|
|
88
|
+
? ["text", "image"] : ["text"],
|
|
89
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
90
|
+
contextWindow,
|
|
91
|
+
maxTokens,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return [...models.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
95
|
+
}
|
|
96
|
+
/** Prefer requested reasoning support, then a larger context/output budget for coding tasks. */
|
|
97
|
+
function automaticModelOrder(models, requirements) {
|
|
98
|
+
return [...models].sort((a, b) => (requirements.preferReasoning ? Number(b.reasoning) - Number(a.reasoning) : 0)
|
|
99
|
+
|| b.contextWindow - a.contextWindow
|
|
100
|
+
|| b.maxTokens - a.maxTokens
|
|
101
|
+
|| a.id.localeCompare(b.id));
|
|
102
|
+
}
|
|
103
|
+
/** Server-owned credentials and last successful catalog; never writes Pi's auth/models files. */
|
|
104
|
+
export class OpenRouterFreeModelsService {
|
|
105
|
+
storage;
|
|
106
|
+
fetchImpl;
|
|
107
|
+
now;
|
|
108
|
+
models = [];
|
|
109
|
+
lastCheckedAt = null;
|
|
110
|
+
lastSyncedAt = null;
|
|
111
|
+
error = null;
|
|
112
|
+
candidateCount = 0;
|
|
113
|
+
rejectedCount = 0;
|
|
114
|
+
revision = 0;
|
|
115
|
+
catalogSequence = 0;
|
|
116
|
+
committedSequence = 0;
|
|
117
|
+
credentialController = new AbortController();
|
|
118
|
+
pending = null;
|
|
119
|
+
controller = null;
|
|
120
|
+
timer = null;
|
|
121
|
+
disposed = false;
|
|
122
|
+
listeners = new Set();
|
|
123
|
+
constructor(storage, fetchImpl = globalThis.fetch, now = () => new Date()) {
|
|
124
|
+
this.storage = storage;
|
|
125
|
+
this.fetchImpl = fetchImpl;
|
|
126
|
+
this.now = now;
|
|
127
|
+
try {
|
|
128
|
+
const cached = record(JSON.parse(storage.getConfigValue(CACHE_KEY) ?? "{}"));
|
|
129
|
+
// Restore through the same validator rather than trusting stored runtime model fields.
|
|
130
|
+
if (this.key() && cached.body && cached.probeVersion === PROBE_VERSION) {
|
|
131
|
+
this.models = parseOpenRouterFreeModels(cached.body);
|
|
132
|
+
this.candidateCount = Math.max(this.models.length, Number(cached.candidateCount) || 0);
|
|
133
|
+
this.rejectedCount = this.candidateCount - this.models.length;
|
|
134
|
+
}
|
|
135
|
+
if (cached.probeVersion === PROBE_VERSION && typeof cached.lastSyncedAt === "string" && Number.isFinite(Date.parse(cached.lastSyncedAt))) {
|
|
136
|
+
this.lastSyncedAt = cached.lastSyncedAt;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch { /* A malformed cache can be rediscovered. */ }
|
|
140
|
+
}
|
|
141
|
+
key() { return this.storage.getConnectorToken("openrouter") ?? ""; }
|
|
142
|
+
status() {
|
|
143
|
+
return {
|
|
144
|
+
configured: !!this.key(), group: OPENROUTER_FREE_GROUP, modelCount: this.models.length,
|
|
145
|
+
lastCheckedAt: this.lastCheckedAt, lastSyncedAt: this.lastSyncedAt, lastError: this.error,
|
|
146
|
+
refreshIntervalHours: OPENROUTER_REFRESH_MS / 3600000,
|
|
147
|
+
candidateCount: this.candidateCount, rejectedCount: this.rejectedCount,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catalog() {
|
|
151
|
+
if (this.disposed || !this.models.length || !this.key())
|
|
152
|
+
return [];
|
|
153
|
+
// One selectable pool, not a category that still requires choosing a concrete model.
|
|
154
|
+
const efforts = this.models.some((model) => model.reasoning)
|
|
155
|
+
? ["off", "minimal", "low", "medium", "high"] : ["off"];
|
|
156
|
+
return [{
|
|
157
|
+
id: OPENROUTER_FREE_SELECTOR,
|
|
158
|
+
label: OPENROUTER_FREE_GROUP,
|
|
159
|
+
reasoningEfforts: efforts.map((effort) => ({ effort })),
|
|
160
|
+
}];
|
|
161
|
+
}
|
|
162
|
+
/** Concrete verified members for the group editor; no key or provider payload is exposed. */
|
|
163
|
+
members() {
|
|
164
|
+
if (this.disposed || !this.key())
|
|
165
|
+
return [];
|
|
166
|
+
return this.orderedModels(this.models, {}).map((model) => ({
|
|
167
|
+
id: `${OPENROUTER_FREE_PROVIDER}/${model.id}`, label: `${model.name} · ${model.id}`,
|
|
168
|
+
reasoningEfforts: (model.reasoning ? ["off", "minimal", "low", "medium", "high"] : ["off"])
|
|
169
|
+
.map((effort) => ({ effort })),
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
orderedModels(models, requirements) {
|
|
173
|
+
const groups = normalizeModelGroups(this.storage.getPreference("pref:modelGroups", []));
|
|
174
|
+
const automatic = automaticModelOrder(models, requirements).filter((model) => !requirements.allowedSelectors
|
|
175
|
+
|| requirements.allowedSelectors.includes(`${OPENROUTER_FREE_PROVIDER}/${model.id}`));
|
|
176
|
+
const order = orderModelIds(automatic.map((model) => `${OPENROUTER_FREE_PROVIDER}/${model.id}`), requirements.allowedSelectors ?? freeModelOrder(groups));
|
|
177
|
+
const byId = new Map(automatic.map((model) => [`${OPENROUTER_FREE_PROVIDER}/${model.id}`, model]));
|
|
178
|
+
return order.map((id) => byId.get(id));
|
|
179
|
+
}
|
|
180
|
+
resolve(selector, requirements = {}) {
|
|
181
|
+
if (this.disposed || !isOpenRouterFreeSelector(selector))
|
|
182
|
+
return null;
|
|
183
|
+
const id = selector.slice(OPENROUTER_FREE_PROVIDER.length + 1);
|
|
184
|
+
const model = selector === OPENROUTER_FREE_SELECTOR
|
|
185
|
+
? this.orderedModels(this.models, requirements)[0] : this.models.find((entry) => entry.id === id);
|
|
186
|
+
const apiKey = this.key();
|
|
187
|
+
return model && apiKey ? { model: structuredClone(model), apiKey } : null;
|
|
188
|
+
}
|
|
189
|
+
onChanged(listener) {
|
|
190
|
+
this.listeners.add(listener);
|
|
191
|
+
return () => { this.listeners.delete(listener); };
|
|
192
|
+
}
|
|
193
|
+
notify() {
|
|
194
|
+
for (const listener of this.listeners) {
|
|
195
|
+
try {
|
|
196
|
+
listener();
|
|
197
|
+
}
|
|
198
|
+
catch { /* An observer must not invalidate a successful sync. */ }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async saveKey(value) {
|
|
202
|
+
if (typeof value !== "string" || !value.trim() || value.length > 512 || /\s/.test(value.trim())) {
|
|
203
|
+
throw new TypeError("请填写有效的 OpenRouter Key。");
|
|
204
|
+
}
|
|
205
|
+
const key = value.trim();
|
|
206
|
+
this.revision++;
|
|
207
|
+
this.credentialController.abort();
|
|
208
|
+
this.credentialController = new AbortController();
|
|
209
|
+
this.controller?.abort();
|
|
210
|
+
const changedKey = key !== this.key();
|
|
211
|
+
this.storage.saveConnector("openrouter", { token: key, apiUrl: "https://openrouter.ai/api/v1" });
|
|
212
|
+
if (changedKey) {
|
|
213
|
+
// Validation belongs to the credential used for the probe.
|
|
214
|
+
this.models = [];
|
|
215
|
+
this.candidateCount = 0;
|
|
216
|
+
this.rejectedCount = 0;
|
|
217
|
+
this.lastSyncedAt = null;
|
|
218
|
+
this.storage.setConfigValue(CACHE_KEY, "{}");
|
|
219
|
+
this.notify();
|
|
220
|
+
}
|
|
221
|
+
this.error = null;
|
|
222
|
+
return this.refresh();
|
|
223
|
+
}
|
|
224
|
+
clearKey() {
|
|
225
|
+
this.revision++;
|
|
226
|
+
this.credentialController.abort();
|
|
227
|
+
this.credentialController = new AbortController();
|
|
228
|
+
this.controller?.abort();
|
|
229
|
+
this.storage.deleteConnector("openrouter");
|
|
230
|
+
this.storage.setConfigValue(CACHE_KEY, "{}");
|
|
231
|
+
this.models = [];
|
|
232
|
+
this.candidateCount = 0;
|
|
233
|
+
this.rejectedCount = 0;
|
|
234
|
+
this.lastCheckedAt = null;
|
|
235
|
+
this.lastSyncedAt = null;
|
|
236
|
+
this.error = null;
|
|
237
|
+
this.notify();
|
|
238
|
+
return this.status();
|
|
239
|
+
}
|
|
240
|
+
refresh() {
|
|
241
|
+
if (this.disposed)
|
|
242
|
+
return Promise.resolve(this.closedStatus());
|
|
243
|
+
if (!this.key())
|
|
244
|
+
return Promise.resolve(this.status());
|
|
245
|
+
if (this.pending?.revision === this.revision)
|
|
246
|
+
return this.pending.promise;
|
|
247
|
+
const revision = this.revision;
|
|
248
|
+
const promise = this.performRefresh(revision);
|
|
249
|
+
this.pending = { revision, promise };
|
|
250
|
+
return promise.finally(() => { if (this.pending?.promise === promise)
|
|
251
|
+
this.pending = null; });
|
|
252
|
+
}
|
|
253
|
+
async performRefresh(revision) {
|
|
254
|
+
const controller = new AbortController();
|
|
255
|
+
this.controller = controller;
|
|
256
|
+
const sequence = ++this.catalogSequence;
|
|
257
|
+
const current = () => !this.disposed && this.revision === revision && sequence >= this.committedSequence;
|
|
258
|
+
try {
|
|
259
|
+
const candidates = await this.fetchCandidates(this.key(), controller.signal);
|
|
260
|
+
if (!current())
|
|
261
|
+
return this.disposed ? this.closedStatus() : this.status();
|
|
262
|
+
// A continuous free price and an earlier successful probe allow reuse.
|
|
263
|
+
const verified = new Set(this.models.map((model) => model.id));
|
|
264
|
+
const newModels = await this.validateModels(candidates.filter((model) => !verified.has(model.id)), this.key(), controller.signal);
|
|
265
|
+
if (!current())
|
|
266
|
+
return this.disposed ? this.closedStatus() : this.status();
|
|
267
|
+
const passed = new Set([...verified, ...newModels.map((model) => model.id)]);
|
|
268
|
+
this.publish(candidates, candidates.filter((model) => passed.has(model.id)), sequence);
|
|
269
|
+
}
|
|
270
|
+
catch (cause) {
|
|
271
|
+
if (current()) {
|
|
272
|
+
this.lastCheckedAt = this.now().toISOString();
|
|
273
|
+
// Network/provider errors may echo credentials: only emit our own bounded messages.
|
|
274
|
+
this.error = cause instanceof OpenRouterSyncError
|
|
275
|
+
? cause.message : "OpenRouter 同步失败,请检查网络后重试。";
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
finally {
|
|
279
|
+
if (this.controller === controller)
|
|
280
|
+
this.controller = null;
|
|
281
|
+
}
|
|
282
|
+
return this.disposed ? this.closedStatus() : this.status();
|
|
283
|
+
}
|
|
284
|
+
async fetchCandidates(apiKey, signal) {
|
|
285
|
+
const response = await this.fetchImpl("https://openrouter.ai/api/v1/models/user", {
|
|
286
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
287
|
+
redirect: "error",
|
|
288
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(15000)]),
|
|
289
|
+
});
|
|
290
|
+
if (!response.ok) {
|
|
291
|
+
await response.body?.cancel();
|
|
292
|
+
throw new OpenRouterSyncError(response.status === 401 || response.status === 403
|
|
293
|
+
? "OpenRouter Key 无效或没有访问权限。"
|
|
294
|
+
: `OpenRouter 价格检查失败(HTTP ${response.status})。`);
|
|
295
|
+
}
|
|
296
|
+
return parseOpenRouterFreeModels(await readJsonResponse(response, MAX_BYTES));
|
|
297
|
+
}
|
|
298
|
+
publish(candidates, models, sequence) {
|
|
299
|
+
const checkedAt = this.now().toISOString();
|
|
300
|
+
const body = { data: models.map((model) => ({
|
|
301
|
+
id: model.id, name: model.name, context_length: model.contextWindow,
|
|
302
|
+
top_provider: { max_completion_tokens: model.maxTokens },
|
|
303
|
+
pricing: { prompt: "0", completion: "0" },
|
|
304
|
+
supported_parameters: model.reasoning ? ["tools", "reasoning"] : ["tools"],
|
|
305
|
+
architecture: { input_modalities: model.input, output_modalities: ["text"] },
|
|
306
|
+
})) };
|
|
307
|
+
this.storage.setConfigValue(CACHE_KEY, JSON.stringify({ body, lastSyncedAt: checkedAt,
|
|
308
|
+
probeVersion: PROBE_VERSION, candidateCount: candidates.length }));
|
|
309
|
+
this.models = models;
|
|
310
|
+
this.candidateCount = candidates.length;
|
|
311
|
+
this.rejectedCount = candidates.length - models.length;
|
|
312
|
+
this.lastCheckedAt = checkedAt;
|
|
313
|
+
this.lastSyncedAt = checkedAt;
|
|
314
|
+
this.error = null;
|
|
315
|
+
this.committedSequence = sequence;
|
|
316
|
+
this.notify();
|
|
317
|
+
}
|
|
318
|
+
/** Fresh pricing is mandatory for each inference, including tool-loop continuations. */
|
|
319
|
+
async resolveForCall(selector, signal, requirements = {}) {
|
|
320
|
+
if (this.disposed || !isOpenRouterFreeSelector(selector))
|
|
321
|
+
throw new Error("免费模型服务不可用。");
|
|
322
|
+
const apiKey = this.key();
|
|
323
|
+
if (!apiKey)
|
|
324
|
+
throw new Error("OpenRouter Key 未配置,请先保存 Key。");
|
|
325
|
+
const revision = this.revision;
|
|
326
|
+
const sequence = ++this.catalogSequence;
|
|
327
|
+
const callSignal = signal
|
|
328
|
+
? AbortSignal.any([signal, this.credentialController.signal]) : this.credentialController.signal;
|
|
329
|
+
const assertCurrent = () => {
|
|
330
|
+
if (this.disposed || this.revision !== revision || apiKey !== this.key()
|
|
331
|
+
|| callSignal.aborted || sequence < this.committedSequence) {
|
|
332
|
+
throw new Error("免费模型配置或目录已变化,本次调用已停止,请重试。");
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
let candidates;
|
|
336
|
+
try {
|
|
337
|
+
candidates = await this.fetchCandidates(apiKey, callSignal);
|
|
338
|
+
}
|
|
339
|
+
catch (cause) {
|
|
340
|
+
// Never proceed with stale prices when a fresh lookup fails.
|
|
341
|
+
const message = cause instanceof OpenRouterSyncError
|
|
342
|
+
? cause.message : "OpenRouter 价格检查失败,本次调用已停止,请重试。";
|
|
343
|
+
if (!this.disposed && this.revision === revision && sequence >= this.committedSequence) {
|
|
344
|
+
this.lastCheckedAt = this.now().toISOString();
|
|
345
|
+
this.error = message;
|
|
346
|
+
}
|
|
347
|
+
throw new Error(message);
|
|
348
|
+
}
|
|
349
|
+
assertCurrent();
|
|
350
|
+
const verifiedIds = new Set(this.models.map((model) => model.id));
|
|
351
|
+
const verified = candidates.filter((model) => verifiedIds.has(model.id));
|
|
352
|
+
// Removing a paid/missing entry also invalidates its earlier usability proof.
|
|
353
|
+
this.publish(candidates, verified, sequence);
|
|
354
|
+
const id = selector.slice(OPENROUTER_FREE_PROVIDER.length + 1);
|
|
355
|
+
const automatic = selector === OPENROUTER_FREE_SELECTOR;
|
|
356
|
+
const permitted = (model) => !requirements.allowedSelectors
|
|
357
|
+
|| requirements.allowedSelectors.includes(`${OPENROUTER_FREE_PROVIDER}/${model.id}`);
|
|
358
|
+
let chosen = (automatic ? undefined : verified.find((model) => model.id === id && permitted(model)))
|
|
359
|
+
?? this.orderedModels(verified, requirements)[0];
|
|
360
|
+
if (!chosen) {
|
|
361
|
+
const preferred = automatic ? undefined : candidates.find((model) => model.id === id && permitted(model));
|
|
362
|
+
const alternatives = this.orderedModels(candidates, requirements);
|
|
363
|
+
const ordered = preferred ? [preferred, ...alternatives.filter((model) => model.id !== id)] : alternatives;
|
|
364
|
+
for (const candidate of ordered) {
|
|
365
|
+
const usable = await this.probeModel(candidate, apiKey, callSignal);
|
|
366
|
+
assertCurrent();
|
|
367
|
+
if (usable) {
|
|
368
|
+
chosen = candidate;
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (chosen)
|
|
373
|
+
this.publish(candidates, [chosen], sequence);
|
|
374
|
+
}
|
|
375
|
+
if (!chosen)
|
|
376
|
+
throw new Error("当前没有已验证且免费的模型,本次调用已停止,请稍后同步免费分组。");
|
|
377
|
+
assertCurrent();
|
|
378
|
+
return { model: structuredClone(chosen), apiKey };
|
|
379
|
+
}
|
|
380
|
+
/** Every discovered model must answer a real message before it becomes selectable. */
|
|
381
|
+
async validateModels(candidates, apiKey, signal) {
|
|
382
|
+
const passed = new Set();
|
|
383
|
+
let next = 0;
|
|
384
|
+
await Promise.all(Array.from({ length: Math.min(PROBE_CONCURRENCY, candidates.length) }, async () => {
|
|
385
|
+
while (!signal.aborted) {
|
|
386
|
+
const model = candidates[next++];
|
|
387
|
+
if (!model)
|
|
388
|
+
break;
|
|
389
|
+
if (await this.probeModel(model, apiKey, signal))
|
|
390
|
+
passed.add(model.id);
|
|
391
|
+
}
|
|
392
|
+
}));
|
|
393
|
+
return candidates.filter((model) => passed.has(model.id));
|
|
394
|
+
}
|
|
395
|
+
async probeModel(model, apiKey, signal) {
|
|
396
|
+
try {
|
|
397
|
+
const response = await this.fetchImpl("https://openrouter.ai/api/v1/chat/completions", {
|
|
398
|
+
method: "POST",
|
|
399
|
+
headers: { Accept: "application/json", "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
400
|
+
redirect: "error",
|
|
401
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(PROBE_TIMEOUT_MS)]),
|
|
402
|
+
body: JSON.stringify({
|
|
403
|
+
model: model.id,
|
|
404
|
+
stream: false,
|
|
405
|
+
max_tokens: Math.min(512, model.maxTokens),
|
|
406
|
+
messages: [
|
|
407
|
+
{ role: "system", content: "你是标题助手。只返回简短标题,不要解释,不要使用工具。" },
|
|
408
|
+
{ role: "user", content: "请为这段对话拟一个简短标题:用户正在设置免费AI模型,并验证模型是否可以正常回复。" },
|
|
409
|
+
],
|
|
410
|
+
// Exercise a tool-capable request without granting executable tools.
|
|
411
|
+
tools: [{ type: "function", function: {
|
|
412
|
+
name: "connectivity_check", description: "连接测试占位工具,请勿调用。",
|
|
413
|
+
parameters: { type: "object", properties: {} },
|
|
414
|
+
} }],
|
|
415
|
+
provider: OPENROUTER_FREE_ROUTING,
|
|
416
|
+
...(model.reasoning ? { reasoning: { effort: "none" } } : {}),
|
|
417
|
+
}),
|
|
418
|
+
});
|
|
419
|
+
if (!response.ok) {
|
|
420
|
+
await response.body?.cancel();
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
const body = record(await readJsonResponse(response, 64 * 1024));
|
|
424
|
+
if (body.error || !Array.isArray(body.choices) || body.choices.length === 0)
|
|
425
|
+
return false;
|
|
426
|
+
const choice = record(body.choices[0]);
|
|
427
|
+
const message = record(choice.message);
|
|
428
|
+
return choice.finish_reason === "stop" && typeof message.content === "string"
|
|
429
|
+
&& message.content.trim().length > 0 && !message.refusal
|
|
430
|
+
&& (!Array.isArray(message.tool_calls) || message.tool_calls.length === 0);
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
// Provider/network failures can echo secrets. Retain only the pass/fail result.
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
closedStatus() {
|
|
438
|
+
return { configured: false, group: OPENROUTER_FREE_GROUP, modelCount: 0, lastCheckedAt: null,
|
|
439
|
+
lastSyncedAt: null, lastError: null, refreshIntervalHours: 6, candidateCount: 0, rejectedCount: 0 };
|
|
440
|
+
}
|
|
441
|
+
start() {
|
|
442
|
+
if (this.timer || this.disposed)
|
|
443
|
+
return;
|
|
444
|
+
void this.refresh();
|
|
445
|
+
this.timer = setInterval(() => { void this.refresh(); }, OPENROUTER_REFRESH_MS);
|
|
446
|
+
this.timer.unref();
|
|
447
|
+
}
|
|
448
|
+
dispose() {
|
|
449
|
+
this.disposed = true;
|
|
450
|
+
this.revision++;
|
|
451
|
+
this.credentialController.abort();
|
|
452
|
+
this.controller?.abort();
|
|
453
|
+
if (this.timer)
|
|
454
|
+
clearInterval(this.timer);
|
|
455
|
+
this.timer = null;
|
|
456
|
+
this.listeners.clear();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Public model selection identity shared by the server and browser; contains no credentials. */
|
|
2
|
+
export declare const OPENROUTER_FREE_PROVIDER = "wand-openrouter-free";
|
|
3
|
+
export declare const OPENROUTER_FREE_GROUP = "\u514D\u8D39\u5206\u7EC4";
|
|
4
|
+
export declare const OPENROUTER_FREE_SELECTOR = "wand-openrouter-free/auto";
|
|
5
|
+
export declare function isOpenRouterFreeSelector(selector: string | null | undefined): boolean;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Public model selection identity shared by the server and browser; contains no credentials. */
|
|
2
|
+
export const OPENROUTER_FREE_PROVIDER = "wand-openrouter-free";
|
|
3
|
+
export const OPENROUTER_FREE_GROUP = "免费分组";
|
|
4
|
+
export const OPENROUTER_FREE_SELECTOR = `${OPENROUTER_FREE_PROVIDER}/auto`;
|
|
5
|
+
export function isOpenRouterFreeSelector(selector) {
|
|
6
|
+
return selector?.startsWith(`${OPENROUTER_FREE_PROVIDER}/`) === true;
|
|
7
|
+
}
|
package/dist/path-repair.d.ts
CHANGED
|
@@ -21,6 +21,12 @@
|
|
|
21
21
|
* "前插"到 process.env.PATH 前面。这
|
|
22
22
|
* 是真正能修好"unit 里 PATH 太旧、用户 shell 里却好好的"这种场景的关键。
|
|
23
23
|
*
|
|
24
|
+
* 同一次探测还回收登录 shell 的**完整环境**(rc 里 export 的变量),交给
|
|
25
|
+
* src/env-utils.ts 作为所有 CLI 子进程的底座:服务进程 env 与 shell env 冲突时
|
|
26
|
+
* 保留进程值(本轮显式启动的参数最大),shell 只填补进程里没有的变量。PATH 顺序
|
|
27
|
+
* 由上面第 2 层单独重排,所以两条链路的 CLI 版本一致。
|
|
28
|
+
* WAND_SHELL_ENV_DISABLE=1 只关掉环境回收,不影响 PATH 修复。
|
|
29
|
+
*
|
|
24
30
|
* 设计决策:
|
|
25
31
|
* - 同步那层只追加不前插:尊重用户已有顺序;异步那层愿意前插,因为它拿到的是
|
|
26
32
|
* 用户实际"会用的"PATH,比 unit 里的更可信。
|
|
@@ -42,6 +48,10 @@ export interface PathRepairResult {
|
|
|
42
48
|
finalPath: string;
|
|
43
49
|
/** login shell 探测阶段的状态:success / disabled / failed / skipped。 */
|
|
44
50
|
deepProbe: "success" | "disabled" | "failed" | "skipped";
|
|
51
|
+
/** login shell 探测实际使用的 shell;用于确认服务环境与用户默认 shell 对齐。 */
|
|
52
|
+
shell?: string;
|
|
53
|
+
/** 登录 shell 里进程环境没有、因此新回收给子进程的变量数。 */
|
|
54
|
+
shellEnvVars?: number;
|
|
45
55
|
/** 异步阶段产生的告警信息(login shell 超时、解析失败等)。 */
|
|
46
56
|
warnings: string[];
|
|
47
57
|
}
|