agentsw 0.10.2 → 0.11.1
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 +56 -18
- package/README.zh-CN.md +43 -12
- package/dist/commands.js +358 -76
- package/dist/discover.js +54 -10
- package/dist/i18n.js +38 -13
- package/dist/index.js +13 -3
- package/dist/menu.js +39 -8
- package/dist/metadata.js +35 -5
- package/dist/target-transaction.js +2 -2
- package/dist/targets/claudecode.js +2 -0
- package/dist/targets/dsh.js +3 -2
- package/dist/targets/omp.js +4 -21
- package/dist/targets/pistyle.js +31 -39
- package/dist/targets/wire.js +65 -0
- package/dist/targets/workbuddy.js +7 -3
- package/dist/ui.js +5 -3
- package/package.json +1 -1
package/dist/targets/pistyle.js
CHANGED
|
@@ -1,22 +1,14 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { backupFile, writeFileAtomic } from "../fsutil.js";
|
|
3
|
+
import { backupFile, SafeConfigError, writeFileAtomic } from "../fsutil.js";
|
|
4
4
|
import { piDir, piModelsFile, piSettingsFile } from "../app-paths.js";
|
|
5
5
|
import { editJsoncObject, isJsonObject, readJsoncObject } from "../jsonc.js";
|
|
6
6
|
import { looksLikeEnvName } from "../slug.js";
|
|
7
7
|
import { transactionalTarget } from "../target-transaction.js";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"name",
|
|
13
|
-
"reasoning",
|
|
14
|
-
"thinkingLevelMap",
|
|
15
|
-
"input",
|
|
16
|
-
"contextWindow",
|
|
17
|
-
"maxTokens",
|
|
18
|
-
"cost",
|
|
19
|
-
];
|
|
8
|
+
import { t } from "../i18n.js";
|
|
9
|
+
import { apiValue, applyDeepseekCompat, classifyApi, entryApi, mergeModels, OWNED_MODEL_METADATA_KEYS, ownedModelMetadata, sdkBaseUrl, stripConflictingOverrides, } from "./wire.js";
|
|
10
|
+
/** pi writes the shared metadata plus its own thinking-level map. */
|
|
11
|
+
const OWNED_MODEL_KEYS = [...OWNED_MODEL_METADATA_KEYS, "thinkingLevelMap"];
|
|
20
12
|
function readModels(file) {
|
|
21
13
|
const document = readJsoncObject(file);
|
|
22
14
|
const providers = document.value.providers;
|
|
@@ -64,11 +56,12 @@ export function piStyleTarget(opts) {
|
|
|
64
56
|
id: opts.id,
|
|
65
57
|
name: opts.name,
|
|
66
58
|
protocols: ["openai", "anthropic"],
|
|
59
|
+
supportsKeepDefault: true,
|
|
67
60
|
get configPaths() {
|
|
68
61
|
return [piModelsFile(id), piSettingsFile(id)];
|
|
69
62
|
},
|
|
70
63
|
detect: () => fs.existsSync(path.dirname(resolveDir())),
|
|
71
|
-
async apply(provider) {
|
|
64
|
+
async apply(provider, options = {}) {
|
|
72
65
|
const dir = resolveDir();
|
|
73
66
|
const modelsFile = path.join(dir, "models.json");
|
|
74
67
|
const settingsFile = path.join(dir, "settings.json");
|
|
@@ -76,6 +69,11 @@ export function piStyleTarget(opts) {
|
|
|
76
69
|
// Validate both files before preparing any writes, including a malformed settings file.
|
|
77
70
|
const modelsDocument = readModels(modelsFile);
|
|
78
71
|
const settingsDocument = readSettings(settingsFile);
|
|
72
|
+
const current = settingsDocument.value;
|
|
73
|
+
if (options.keepDefault && current.defaultProvider === provider.id && current.defaultModel &&
|
|
74
|
+
!provider.models.some((model) => model.id === current.defaultModel)) {
|
|
75
|
+
throw new SafeConfigError(t("sync.keepDefaultMissing", { file: settingsFile, id: provider.id, model: current.defaultModel }));
|
|
76
|
+
}
|
|
79
77
|
const modelsConfig = { ...modelsDocument.value };
|
|
80
78
|
const providers = { ...modelsConfig.providers };
|
|
81
79
|
// Keys agentsw does not model (headers, authHeader, oauth, ...) and per-model extras
|
|
@@ -84,26 +82,17 @@ export function piStyleTarget(opts) {
|
|
|
84
82
|
const api = apiValue(provider.protocol, provider.openaiApi, prev.api ?? entryApi(prev));
|
|
85
83
|
const baseUrl = sdkBaseUrl(provider.protocol, provider.baseUrl);
|
|
86
84
|
const models = mergeModels(prev.models, provider.models.map((m) => ({
|
|
87
|
-
|
|
88
|
-
...(m.name ? { name: m.name } : {}),
|
|
89
|
-
...(m.reasoning !== undefined ? { reasoning: m.reasoning } : {}),
|
|
85
|
+
...ownedModelMetadata(m),
|
|
90
86
|
...(m.reasoning && m.reasoningEfforts?.length
|
|
91
87
|
? { thinkingLevelMap: thinkingLevelMap(m.reasoningEfforts) }
|
|
92
88
|
: {}),
|
|
93
|
-
input: m.imageInput ? ["text", "image"] : ["text"],
|
|
94
|
-
...(m.contextWindow ? { contextWindow: m.contextWindow } : {}),
|
|
95
|
-
...(m.maxOutput ? { maxTokens: m.maxOutput } : {}),
|
|
96
|
-
...(m.cost
|
|
97
|
-
? {
|
|
98
|
-
cost: {
|
|
99
|
-
input: m.cost.input ?? 0,
|
|
100
|
-
output: m.cost.output ?? 0,
|
|
101
|
-
cacheRead: m.cost.cacheRead ?? 0,
|
|
102
|
-
cacheWrite: m.cost.cacheWrite ?? 0,
|
|
103
|
-
},
|
|
104
|
-
}
|
|
105
|
-
: {}),
|
|
106
89
|
})), OWNED_MODEL_KEYS);
|
|
90
|
+
// pi/prime key their DeepSeek wire rules by provider name, so a gateway
|
|
91
|
+
// entry never picks them up; the flags below are what their own deepseek
|
|
92
|
+
// catalog entries carry. Anthropic-wire entries replay differently.
|
|
93
|
+
if (provider.protocol === "openai") {
|
|
94
|
+
applyDeepseekCompat(models, { requiresReasoningContentOnAssistantMessages: true, thinkingFormat: "deepseek" });
|
|
95
|
+
}
|
|
107
96
|
const conflicts = stripConflictingOverrides(models, api, baseUrl);
|
|
108
97
|
if (conflicts.length)
|
|
109
98
|
notes.push(`dropped model overrides pointing elsewhere: ${conflicts.join(", ")}`);
|
|
@@ -116,22 +105,25 @@ export function piStyleTarget(opts) {
|
|
|
116
105
|
models,
|
|
117
106
|
};
|
|
118
107
|
modelsConfig.providers = providers;
|
|
119
|
-
const
|
|
108
|
+
const modelsText = editJsoncObject(modelsDocument, modelsConfig);
|
|
109
|
+
const settingsText = options.keepDefault ? undefined : editJsoncObject(settingsDocument, {
|
|
120
110
|
...settingsDocument.value,
|
|
121
111
|
defaultProvider: provider.id,
|
|
122
112
|
defaultModel: provider.defaultModel,
|
|
123
|
-
};
|
|
124
|
-
const
|
|
125
|
-
const settingsText = editJsoncObject(settingsDocument, settings);
|
|
113
|
+
});
|
|
114
|
+
const changed = [modelsFile];
|
|
126
115
|
const modelsBackup = backupFile(modelsFile);
|
|
127
116
|
if (modelsBackup)
|
|
128
117
|
notes.push(`backup: ${modelsBackup}`);
|
|
129
118
|
writeFileAtomic(modelsFile, modelsText);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
119
|
+
if (settingsText !== undefined) {
|
|
120
|
+
const settingsBackup = backupFile(settingsFile);
|
|
121
|
+
if (settingsBackup)
|
|
122
|
+
notes.push(`backup: ${settingsBackup}`);
|
|
123
|
+
writeFileAtomic(settingsFile, settingsText);
|
|
124
|
+
changed.push(settingsFile);
|
|
125
|
+
}
|
|
126
|
+
return { app: opts.id, changed, notes };
|
|
135
127
|
},
|
|
136
128
|
async prune(provider) {
|
|
137
129
|
const dir = resolveDir();
|
package/dist/targets/wire.js
CHANGED
|
@@ -99,6 +99,11 @@ derived = ["input"]) {
|
|
|
99
99
|
const old = prev.get(m.id);
|
|
100
100
|
if (!old)
|
|
101
101
|
return m;
|
|
102
|
+
// An entry that carries nothing but its id says nothing about the model: it
|
|
103
|
+
// is no evidence that the config's metadata went stale, so clearing owned
|
|
104
|
+
// keys on its behalf would delete what a richer sync or the user put there.
|
|
105
|
+
if (Object.keys(m).length === 1)
|
|
106
|
+
return { ...m, ...old };
|
|
102
107
|
const kept = { ...old };
|
|
103
108
|
const known = informative.some((key) => key in m);
|
|
104
109
|
if (known)
|
|
@@ -108,6 +113,66 @@ derived = ["input"]) {
|
|
|
108
113
|
return { ...kept, ...m };
|
|
109
114
|
});
|
|
110
115
|
}
|
|
116
|
+
/** Per-model keys every pi-family adapter owns; see `ownedModelMetadata`. */
|
|
117
|
+
export const OWNED_MODEL_METADATA_KEYS = ["id", "name", "reasoning", "input", "contextWindow", "maxTokens", "cost"];
|
|
118
|
+
const DEEPSEEK_MODEL_ID = /deepseek/i;
|
|
119
|
+
/**
|
|
120
|
+
* omp/pi/prime key their DeepSeek wire rules by provider name ("deepseek",
|
|
121
|
+
* "opencode-go", ...), so a gateway or reseller entry matches none of them: its
|
|
122
|
+
* reasoning replay then breaks and DeepSeek answers 400. Match on the model id
|
|
123
|
+
* instead and write the adapter's own flags per model; an explicit user value wins.
|
|
124
|
+
*/
|
|
125
|
+
export function applyDeepseekCompat(models, flags) {
|
|
126
|
+
for (const model of models) {
|
|
127
|
+
if (!DEEPSEEK_MODEL_ID.test(String(model.id)))
|
|
128
|
+
continue;
|
|
129
|
+
const compat = (model.compat ?? {});
|
|
130
|
+
model.compat = { ...flags, ...compat };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Whether a store entry describes the model at all. A bare id is ignorance,
|
|
135
|
+
* not a catalog statement, so adapters must not clear what the app config
|
|
136
|
+
* already holds on its behalf.
|
|
137
|
+
*/
|
|
138
|
+
export function hasModelMetadata(m) {
|
|
139
|
+
return (m.name !== undefined ||
|
|
140
|
+
m.reasoning !== undefined ||
|
|
141
|
+
m.reasoningEfforts !== undefined ||
|
|
142
|
+
m.imageInput !== undefined ||
|
|
143
|
+
m.contextWindow !== undefined ||
|
|
144
|
+
m.maxInput !== undefined ||
|
|
145
|
+
m.maxOutput !== undefined ||
|
|
146
|
+
m.cost !== undefined);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The per-model metadata agentsw writes into a pi-family models config. omp and
|
|
150
|
+
* the pi/prime adapters share the shape; only the extras around it differ, so
|
|
151
|
+
* keeping one builder is what stops the two from drifting apart.
|
|
152
|
+
*/
|
|
153
|
+
export function ownedModelMetadata(m) {
|
|
154
|
+
// `input` is a guess for a spec that says nothing; writing it would make the
|
|
155
|
+
// entry look authoritative to mergeModels and clear the config's own values.
|
|
156
|
+
const known = hasModelMetadata(m);
|
|
157
|
+
return {
|
|
158
|
+
id: m.id,
|
|
159
|
+
...(m.name ? { name: m.name } : {}),
|
|
160
|
+
...(m.reasoning !== undefined ? { reasoning: m.reasoning } : {}),
|
|
161
|
+
...(known ? { input: m.imageInput ? ["text", "image"] : ["text"] } : {}),
|
|
162
|
+
...(m.contextWindow ? { contextWindow: m.contextWindow } : {}),
|
|
163
|
+
...(m.maxOutput ? { maxTokens: m.maxOutput } : {}),
|
|
164
|
+
...(m.cost
|
|
165
|
+
? {
|
|
166
|
+
cost: {
|
|
167
|
+
input: m.cost.input ?? 0,
|
|
168
|
+
output: m.cost.output ?? 0,
|
|
169
|
+
cacheRead: m.cost.cacheRead ?? 0,
|
|
170
|
+
cacheWrite: m.cost.cacheWrite ?? 0,
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
: {}),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
111
176
|
/**
|
|
112
177
|
* Drop per-model `api`/`baseUrl` overrides that contradict the route being
|
|
113
178
|
* written. A model-level override wins over the provider entry in omp and pi,
|
|
@@ -6,6 +6,7 @@ import { workbuddyDir } from "../app-paths.js";
|
|
|
6
6
|
import { localProviderId } from "../provider-identity.js";
|
|
7
7
|
import { providerIdFromBaseUrl, providerNameFromBaseUrl } from "../slug.js";
|
|
8
8
|
import { transactionalTarget } from "../target-transaction.js";
|
|
9
|
+
import { hasModelMetadata } from "./wire.js";
|
|
9
10
|
function configDir() {
|
|
10
11
|
return workbuddyDir();
|
|
11
12
|
}
|
|
@@ -101,12 +102,15 @@ export const workbuddy = transactionalTarget({
|
|
|
101
102
|
const previous = new Map(owned.map((row) => [row.id, row]));
|
|
102
103
|
const ours = provider.models.map((model) => {
|
|
103
104
|
const custom = { ...previous.get(model.id) };
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
// A bare id says nothing about the model; the row keeps what it already has.
|
|
106
|
+
const known = hasModelMetadata(model);
|
|
107
|
+
if (known)
|
|
108
|
+
for (const key of OWNED_MODEL_KEYS)
|
|
109
|
+
delete custom[key];
|
|
106
110
|
return {
|
|
107
111
|
...custom,
|
|
108
112
|
id: model.id,
|
|
109
|
-
name: model.name ?? model.id,
|
|
113
|
+
name: model.name ?? (known ? undefined : custom.name) ?? model.id,
|
|
110
114
|
vendor: provider.name,
|
|
111
115
|
url,
|
|
112
116
|
apiKey: provider.apiKey,
|
package/dist/ui.js
CHANGED
|
@@ -51,10 +51,12 @@ export function warn(message) {
|
|
|
51
51
|
process.stderr.write(pc.yellow(`warning: ${message}`) + "\n");
|
|
52
52
|
}
|
|
53
53
|
function emitJsonError(message) {
|
|
54
|
-
|
|
54
|
+
emitJson(undefined, message);
|
|
55
55
|
}
|
|
56
|
-
export function emitJson(data) {
|
|
57
|
-
process.stdout.write(JSON.stringify({ ok:
|
|
56
|
+
export function emitJson(data, errorMessage) {
|
|
57
|
+
process.stdout.write(JSON.stringify({ ok: errorMessage === undefined, command, version, data,
|
|
58
|
+
...(errorMessage === undefined ? {} : { error: { message: errorMessage } }),
|
|
59
|
+
}) + "\n");
|
|
58
60
|
}
|
|
59
61
|
/** Fatal error. In machine mode the failure becomes the single stdout document, so pipes stay parseable. */
|
|
60
62
|
export function error(message) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentsw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "CLI provider switcher for coding agents: manage OpenAI/Anthropic-protocol providers in one place and sync them, with models.dev metadata, into Claude Code, Codex, omp, pi, prime-agent, opencode, Hermes, WorkBuddy and DeepSeek Harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|