saros-proxy 0.7.3 → 0.9.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.
Files changed (78) hide show
  1. package/README.md +98 -47
  2. package/dist/cli/autostart.js.map +1 -1
  3. package/dist/cli/daemon.d.ts +31 -4
  4. package/dist/cli/daemon.js +124 -22
  5. package/dist/cli/daemon.js.map +1 -1
  6. package/dist/cli/dsh-config.d.ts +32 -0
  7. package/dist/cli/dsh-config.js +122 -0
  8. package/dist/cli/dsh-config.js.map +1 -0
  9. package/dist/cli/harness-models.d.ts +26 -2
  10. package/dist/cli/harness-models.js +192 -2
  11. package/dist/cli/harness-models.js.map +1 -1
  12. package/dist/cli/harness-sync.d.ts +27 -10
  13. package/dist/cli/harness-sync.js +70 -16
  14. package/dist/cli/harness-sync.js.map +1 -1
  15. package/dist/cli/help.js +4 -2
  16. package/dist/cli/help.js.map +1 -1
  17. package/dist/cli/omp-config.js +22 -5
  18. package/dist/cli/omp-config.js.map +1 -1
  19. package/dist/cli/opencode-config.js +22 -15
  20. package/dist/cli/opencode-config.js.map +1 -1
  21. package/dist/cli/overview.d.ts +10 -0
  22. package/dist/cli/overview.js +61 -0
  23. package/dist/cli/overview.js.map +1 -0
  24. package/dist/cli/pi-config.js +21 -5
  25. package/dist/cli/pi-config.js.map +1 -1
  26. package/dist/cli/setup.d.ts +6 -10
  27. package/dist/cli/setup.js +107 -233
  28. package/dist/cli/setup.js.map +1 -1
  29. package/dist/cli/ui.d.ts +0 -1
  30. package/dist/cli/ui.js +0 -16
  31. package/dist/cli/ui.js.map +1 -1
  32. package/dist/cli/update-check.js.map +1 -1
  33. package/dist/cli/usage.d.ts +12 -0
  34. package/dist/cli/usage.js +116 -0
  35. package/dist/cli/usage.js.map +1 -0
  36. package/dist/config.d.ts +13 -17
  37. package/dist/config.js +29 -78
  38. package/dist/config.js.map +1 -1
  39. package/dist/constants.d.ts +7 -6
  40. package/dist/constants.js +10 -5
  41. package/dist/constants.js.map +1 -1
  42. package/dist/index.d.ts +4 -4
  43. package/dist/index.js +34 -34
  44. package/dist/index.js.map +1 -1
  45. package/dist/model-stats.d.ts +20 -0
  46. package/dist/model-stats.js +135 -0
  47. package/dist/model-stats.js.map +1 -0
  48. package/dist/models-sync.js +10 -4
  49. package/dist/models-sync.js.map +1 -1
  50. package/dist/probe-cache.js.map +1 -1
  51. package/dist/providers/commandcode.d.ts +17 -0
  52. package/dist/providers/commandcode.js +232 -0
  53. package/dist/providers/commandcode.js.map +1 -0
  54. package/dist/providers/index.d.ts +43 -0
  55. package/dist/providers/index.js +75 -0
  56. package/dist/providers/index.js.map +1 -0
  57. package/dist/providers/infer.d.ts +23 -0
  58. package/dist/providers/infer.js +13 -0
  59. package/dist/providers/infer.js.map +1 -0
  60. package/dist/providers/opencode-go.d.ts +4 -0
  61. package/dist/providers/opencode-go.js +179 -0
  62. package/dist/providers/opencode-go.js.map +1 -0
  63. package/dist/providers/types.d.ts +118 -0
  64. package/dist/providers/types.js +10 -0
  65. package/dist/providers/types.js.map +1 -0
  66. package/dist/proxy-logic.d.ts +46 -22
  67. package/dist/proxy-logic.js +146 -106
  68. package/dist/proxy-logic.js.map +1 -1
  69. package/dist/proxy.d.ts +20 -2
  70. package/dist/proxy.js +231 -57
  71. package/dist/proxy.js.map +1 -1
  72. package/dist/usage-client.d.ts +41 -0
  73. package/dist/usage-client.js +124 -0
  74. package/dist/usage-client.js.map +1 -0
  75. package/dist/validation.d.ts +5 -5
  76. package/dist/validation.js +7 -9
  77. package/dist/validation.js.map +1 -1
  78. package/package.json +75 -77
@@ -0,0 +1,32 @@
1
+ /**
2
+ * dsh-config.ts — DeepSeek Harness (dsh) sync target.
3
+ *
4
+ * Updates the saros-proxy provider's model list inside the dsh profile's
5
+ * patch layer (~/.dsh/profiles/web/cordis.patch.yml), row id `llm-pi-ai`.
6
+ * dsh model entries use the pi/omp field names plus a `reasoningEfforts`
7
+ * map ({ 'off': null, high: 'high', … }) instead of pi/omp's `thinking`
8
+ * object — the transformer here converts the canonical entries.
9
+ *
10
+ * Only `providers["saros-proxy"].models` is replaced; every other provider
11
+ * row (commandcode, commandcode-anthropic, agent-default-model, MCP inserts)
12
+ * and all user fields are preserved. Leading comment lines are re-emitted
13
+ * verbatim (the patch file documents its `!!js` allowance in a comment).
14
+ * cordis.yml is NEVER touched (dsh rewrites it empty at every boot).
15
+ */
16
+ import { type PiOmpModel } from './harness-models.js';
17
+ import type { OpencodeConfigResult } from './opencode-config.js';
18
+ /** Default dsh patch layer path for the web profile. */
19
+ export declare function getDefaultDshConfigPath(): string;
20
+ /** Convert a pi/omp model entry into the dsh patch entry shape. */
21
+ export declare function toDshModel(model: PiOmpModel): Record<string, unknown>;
22
+ /**
23
+ * Sync the saros-proxy provider model list into the dsh patch layer.
24
+ *
25
+ * @param modelsMap — Canonical model map (id → opencode-shaped entry)
26
+ * @param port — Proxy port (unused today; kept for harness-sync parity)
27
+ * @param options.configPath — Explicit config path (for testing)
28
+ * @returns Result with success status and path
29
+ */
30
+ export declare function syncModelsToDshConfig(modelsMap: Record<string, unknown>, port: number, options?: {
31
+ configPath?: string;
32
+ }): OpencodeConfigResult;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * dsh-config.ts — DeepSeek Harness (dsh) sync target.
3
+ *
4
+ * Updates the saros-proxy provider's model list inside the dsh profile's
5
+ * patch layer (~/.dsh/profiles/web/cordis.patch.yml), row id `llm-pi-ai`.
6
+ * dsh model entries use the pi/omp field names plus a `reasoningEfforts`
7
+ * map ({ 'off': null, high: 'high', … }) instead of pi/omp's `thinking`
8
+ * object — the transformer here converts the canonical entries.
9
+ *
10
+ * Only `providers["saros-proxy"].models` is replaced; every other provider
11
+ * row (commandcode, commandcode-anthropic, agent-default-model, MCP inserts)
12
+ * and all user fields are preserved. Leading comment lines are re-emitted
13
+ * verbatim (the patch file documents its `!!js` allowance in a comment).
14
+ * cordis.yml is NEVER touched (dsh rewrites it empty at every boot).
15
+ */
16
+ import { existsSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ import { homedir } from 'node:os';
19
+ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
20
+ import { toPiOmpModelArray } from './harness-models.js';
21
+ /** Default dsh patch layer path for the web profile. */
22
+ export function getDefaultDshConfigPath() {
23
+ return join(homedir(), '.dsh', 'profiles', 'web', 'cordis.patch.yml');
24
+ }
25
+ const DSH_ROW_ID = 'llm-pi-ai';
26
+ const SAROS_PROVIDER_KEY = 'saros-proxy';
27
+ /** Convert a pi/omp model entry into the dsh patch entry shape. */
28
+ export function toDshModel(model) {
29
+ const entry = {
30
+ id: model.id,
31
+ name: model.name,
32
+ input: model.input,
33
+ contextWindow: model.contextWindow,
34
+ maxTokens: model.maxTokens,
35
+ };
36
+ if (model.thinking && model.thinking.efforts.length > 0) {
37
+ const efforts = { off: null };
38
+ for (const effort of model.thinking.efforts) {
39
+ efforts[effort] = effort;
40
+ }
41
+ entry.reasoningEfforts = efforts;
42
+ }
43
+ if (model.cost) {
44
+ entry.cost = model.cost;
45
+ }
46
+ return entry;
47
+ }
48
+ /**
49
+ * Split leading comment lines from the raw YAML text so they can be
50
+ * re-emitted after stringify (the yaml package drops comments).
51
+ */
52
+ function splitLeadingComments(raw) {
53
+ const lines = raw.split('\n');
54
+ const comments = [];
55
+ let i = 0;
56
+ while (i < lines.length && (lines[i].trimStart().startsWith('#') || lines[i].trim() === '')) {
57
+ comments.push(lines[i]);
58
+ i++;
59
+ }
60
+ return { comments: comments.join('\n'), body: lines.slice(i).join('\n') };
61
+ }
62
+ /**
63
+ * Sync the saros-proxy provider model list into the dsh patch layer.
64
+ *
65
+ * @param modelsMap — Canonical model map (id → opencode-shaped entry)
66
+ * @param port — Proxy port (unused today; kept for harness-sync parity)
67
+ * @param options.configPath — Explicit config path (for testing)
68
+ * @returns Result with success status and path
69
+ */
70
+ export function syncModelsToDshConfig(modelsMap, port, options = {}) {
71
+ void port;
72
+ const configPath = options.configPath ?? getDefaultDshConfigPath();
73
+ if (!existsSync(configPath)) {
74
+ return { success: false, error: 'config not found at ' + configPath };
75
+ }
76
+ try {
77
+ const raw = readFileSync(configPath, 'utf-8');
78
+ const { comments, body } = splitLeadingComments(raw);
79
+ let doc;
80
+ try {
81
+ doc = parseYaml(body);
82
+ }
83
+ catch {
84
+ return { success: false, path: configPath, error: 'dsh patch contains invalid YAML' };
85
+ }
86
+ if (!Array.isArray(doc)) {
87
+ return { success: false, path: configPath, error: 'dsh patch is not a top-level array' };
88
+ }
89
+ // Locate the llm-pi-ai row and its saros-proxy provider entry.
90
+ const row = doc.find((r) => r && typeof r === 'object' && !Array.isArray(r) &&
91
+ r.id === DSH_ROW_ID);
92
+ if (!row) {
93
+ return { success: false, path: configPath, error: `dsh patch has no ${DSH_ROW_ID} row` };
94
+ }
95
+ const config = row.config;
96
+ const providers = config?.providers;
97
+ const provider = providers?.[SAROS_PROVIDER_KEY];
98
+ if (!provider) {
99
+ return {
100
+ success: false,
101
+ path: configPath,
102
+ error: `dsh patch ${DSH_ROW_ID} row has no ${SAROS_PROVIDER_KEY} provider`,
103
+ };
104
+ }
105
+ // Replace ONLY the models key.
106
+ provider.models = toPiOmpModelArray(modelsMap).map(toDshModel);
107
+ const backupPath = configPath + '.backup';
108
+ if (existsSync(configPath)) {
109
+ copyFileSync(configPath, backupPath);
110
+ }
111
+ writeFileSync(configPath, comments ? comments + '\n' + stringifyYaml(doc) : stringifyYaml(doc), 'utf-8');
112
+ return { success: true, path: configPath, created: false };
113
+ }
114
+ catch (err) {
115
+ return {
116
+ success: false,
117
+ path: configPath,
118
+ error: err instanceof Error ? err.message : String(err),
119
+ };
120
+ }
121
+ }
122
+ //# sourceMappingURL=dsh-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dsh-config.js","sourceRoot":"","sources":["../../src/cli/dsh-config.ts"],"names":[],"mappings":"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"}
@@ -8,6 +8,12 @@
8
8
  * upstream + models.dev metadata (falling back to bundled models.json).
9
9
  */
10
10
  import type { ProxyConfig } from '../config.js';
11
+ /**
12
+ * Suffix for provider-specific model ids in synced harness configs.
13
+ * Commandcode-only models (claude-*, vendor/model) are exposed to harnesses
14
+ * as `<id>@commandcode`; the proxy strips the suffix on the way upstream.
15
+ */
16
+ export declare const PROVIDER_MODEL_SUFFIX = "@";
11
17
  export interface PiOmpModel {
12
18
  id: string;
13
19
  name: string;
@@ -21,6 +27,11 @@ export interface PiOmpModel {
21
27
  cacheRead: number;
22
28
  cacheWrite: number;
23
29
  };
30
+ /** Explicit thinking surface (pi/omp schema). Absent → harness infers one. */
31
+ thinking?: {
32
+ mode: 'effort';
33
+ efforts: string[];
34
+ };
24
35
  }
25
36
  /**
26
37
  * Transform one canonical (opencode-shaped) model entry into the
@@ -30,12 +41,25 @@ export interface PiOmpModel {
30
41
  export declare function toPiOmpModel(entry: Record<string, unknown>): PiOmpModel;
31
42
  /** Transform the canonical model map into a pi/omp `models` array. */
32
43
  export declare function toPiOmpModelArray(models: Record<string, unknown>): PiOmpModel[];
44
+ /**
45
+ * Normalize a provider model id to its bare name for cross-provider
46
+ * matching: 'deepseek/deepseek-v4-flash' → 'deepseek-v4-flash';
47
+ * 'glm-5' → 'glm-5'. Case- and separator-insensitive.
48
+ */
49
+ export declare function bareModelName(modelId: string): string;
50
+ /**
51
+ * Annotate a display name with how many keys can serve the model,
52
+ * e.g. "DeepSeek V4 Flash (3)". Communicates rotation width: '(3)' rotates
53
+ * across 3 accounts (possibly cross-provider), '(1)' is a single key.
54
+ */
55
+ export declare function annotateName(name: string, keyCount: number): string;
33
56
  /**
34
57
  * Build the canonical model map (id → opencode-shaped entry).
35
58
  *
36
59
  * Live path (config provided, not offline): fetch upstream model IDs and
37
- * enrich each with models.dev metadata via buildMinimalStub. Falls back to
38
- * the bundled models.json when the upstream fetch yields nothing.
60
+ * enrich each with models.dev metadata via buildMinimalStub, then append
61
+ * provider-specific catalog models (@commandcode etc.). Falls back to the
62
+ * bundled models.json when the upstream fetch yields nothing.
39
63
  * Offline path (or no config): bundled models.json only.
40
64
  */
41
65
  export declare function buildCanonicalModels(config?: ProxyConfig, opts?: {
@@ -9,6 +9,40 @@
9
9
  */
10
10
  import { loadModelsFromJson } from './opencode-config.js';
11
11
  import { buildMinimalStub, fetchModelsDevMetadata, fetchUpstreamModelIds } from '../models-sync.js';
12
+ import { allProviders, getProvider, inferProvider } from '../providers/index.js';
13
+ /**
14
+ * Suffix for provider-specific model ids in synced harness configs.
15
+ * Commandcode-only models (claude-*, vendor/model) are exposed to harnesses
16
+ * as `<id>@commandcode`; the proxy strips the suffix on the way upstream.
17
+ */
18
+ export const PROVIDER_MODEL_SUFFIX = '@';
19
+ /** Canonical effort vocabulary, least → most intensive (pi/omp schema). */
20
+ const EFFORT_ORDER = ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
21
+ /**
22
+ * Extract the effort ladder from models.dev `reasoning_options`
23
+ * (`[{ type: 'effort', values: [...] }]`). Keeps only values in the pi/omp
24
+ * thinking schema vocabulary; deduplicates and orders least → most intensive.
25
+ */
26
+ function parseEfforts(entry) {
27
+ const options = entry.reasoning_options;
28
+ if (!Array.isArray(options))
29
+ return [];
30
+ const found = new Set();
31
+ for (const option of options) {
32
+ if (!option || typeof option !== 'object' || Array.isArray(option))
33
+ continue;
34
+ if (option.type !== 'effort')
35
+ continue;
36
+ const values = option.values;
37
+ if (!Array.isArray(values))
38
+ continue;
39
+ for (const value of values) {
40
+ if (typeof value === 'string')
41
+ found.add(value);
42
+ }
43
+ }
44
+ return EFFORT_ORDER.filter((effort) => found.has(effort));
45
+ }
12
46
  /**
13
47
  * Transform one canonical (opencode-shaped) model entry into the
14
48
  * pi/omp model shape. Unknown metadata falls back to safe defaults
@@ -28,6 +62,15 @@ export function toPiOmpModel(entry) {
28
62
  contextWindow: typeof limit.context === 'number' ? limit.context : 262144,
29
63
  maxTokens: typeof limit.output === 'number' ? limit.output : 65536,
30
64
  };
65
+ // Explicit thinking metadata: models.dev effort options → pi/omp
66
+ // `thinking.efforts`. Without it the harness infers a generic ladder that
67
+ // can disagree with the model's real contract (e.g. ox-alpha only accepts
68
+ // low/high/max).
69
+ if (model.reasoning) {
70
+ const efforts = parseEfforts(entry);
71
+ if (efforts.length > 0)
72
+ model.thinking = { mode: 'effort', efforts };
73
+ }
31
74
  // Cost is optional; accept both camelCase (pi/omp) and snake_case (models.dev).
32
75
  const rawCost = entry.cost;
33
76
  if (rawCost && typeof rawCost === 'object' && !Array.isArray(rawCost)) {
@@ -49,12 +92,144 @@ export function toPiOmpModel(entry) {
49
92
  export function toPiOmpModelArray(models) {
50
93
  return Object.values(models).map((entry) => toPiOmpModel(entry));
51
94
  }
95
+ /**
96
+ * Normalize a provider model id to its bare name for cross-provider
97
+ * matching: 'deepseek/deepseek-v4-flash' → 'deepseek-v4-flash';
98
+ * 'glm-5' → 'glm-5'. Case- and separator-insensitive.
99
+ */
100
+ export function bareModelName(modelId) {
101
+ const last = modelId.split('/').pop() ?? modelId;
102
+ return last.toLowerCase().replaceAll('_', '-');
103
+ }
104
+ /**
105
+ * Annotate a display name with how many keys can serve the model,
106
+ * e.g. "DeepSeek V4 Flash (3)". Communicates rotation width: '(3)' rotates
107
+ * across 3 accounts (possibly cross-provider), '(1)' is a single key.
108
+ */
109
+ export function annotateName(name, keyCount) {
110
+ if (keyCount <= 0)
111
+ return name;
112
+ return `${name} (${keyCount})`;
113
+ }
114
+ /**
115
+ * Count how many of the user's keys can serve a bare model name.
116
+ *
117
+ * The owning provider (whose catalog the entry came from) always serves it.
118
+ * Every OTHER provider must claim the model definitively ('yes' via
119
+ * bare-name/affinity check) — a structural 'maybe' does not count, or
120
+ * blanket-maybe providers (opencode-go) would overcount models they don't
121
+ * actually have (e.g. claude-*).
122
+ */
123
+ function countServingKeys(config, bareName, owner) {
124
+ return (config.keys ?? []).filter((k) => {
125
+ const provider = getProvider(inferProvider(k));
126
+ if (!provider)
127
+ return false;
128
+ if (provider.id === owner)
129
+ return true;
130
+ const byName = provider.modelAffinityByName?.(bareName);
131
+ if (byName)
132
+ return byName === 'yes';
133
+ return provider.modelAffinity(bareName) === 'yes';
134
+ }).length;
135
+ }
136
+ /**
137
+ * Map one provider-catalog entry into the canonical opencode-shaped entry.
138
+ * Uses the catalog's own fields when present (commandcode serves name +
139
+ * context_length); models.dev metadata is not available for these providers.
140
+ */
141
+ /**
142
+ * Map one provider-catalog entry into the canonical opencode-shaped entry.
143
+ * Uses the catalog's own fields when present (commandcode serves name +
144
+ * context_length); models.dev metadata is not available for these providers.
145
+ */
146
+ function providerCatalogToCanonical(modelId, entry, keyCount) {
147
+ const context = typeof entry.context_length === 'number' ? entry.context_length : undefined;
148
+ const name = typeof entry.name === 'string' ? entry.name : modelId;
149
+ return {
150
+ id: modelId,
151
+ name: annotateName(name, keyCount),
152
+ tool_call: true,
153
+ reasoning: true,
154
+ limit: {
155
+ context: context ?? 262144,
156
+ output: 65536,
157
+ },
158
+ modalities: { input: ['text'], output: ['text'] },
159
+ // Conservative effort ladder for provider-catalog models (no models.dev
160
+ // metadata): the commandcode gateway accepts low|medium|high|xhigh and
161
+ // rejects minimal — better declared here than invented by the harness.
162
+ reasoning_options: [{ type: 'effort', values: ['low', 'medium', 'high'] }],
163
+ // Hint consumed by display tooling; harmless to harnesses that ignore it.
164
+ ...(typeof entry.owned_by === 'string' ? { owned_by: entry.owned_by } : {}),
165
+ };
166
+ }
167
+ /**
168
+ * Fetch model entries from the given providers and merge them into the map.
169
+ *
170
+ * Cross-provider aliasing: when a provider catalog serves the SAME model
171
+ * under a different id (bare name matches an existing entry — e.g.
172
+ * 'deepseek/deepseek-v4-flash' on commandcode vs 'deepseek-v4-flash' on
173
+ * opencode-go), the existing entry gains the new provider as an alias:
174
+ * requests naming either id rotate across all serving keys.
175
+ *
176
+ * Provider-only models (claude-*, unique vendor ids) are added as
177
+ * `<id>@<providerId>` suffixed entries.
178
+ *
179
+ * Only providers the user actually has keys for are merged: exposing
180
+ * provider-only models without a usable key would create broken entries in
181
+ * harness configs.
182
+ */
183
+ async function appendProviderCatalogModels(map, existingIds, config) {
184
+ const configured = new Set((config.keys ?? []).map((k) => inferProvider(k)));
185
+ const providers = allProviders().filter((p) => configured.has(p.id) && typeof p.fetchCatalog === 'function');
186
+ // Bare-name index of what's already in the map (from the primary upstream)
187
+ const byBareName = new Map();
188
+ for (const id of existingIds) {
189
+ const bare = bareModelName(id);
190
+ if (!byBareName.has(bare))
191
+ byBareName.set(bare, id);
192
+ }
193
+ await Promise.all(providers.map(async (provider) => {
194
+ const catalog = await provider.fetchCatalog();
195
+ if (!catalog)
196
+ return;
197
+ for (const entry of catalog) {
198
+ const rawId = typeof entry.id === 'string' ? entry.id : '';
199
+ if (!rawId)
200
+ continue;
201
+ const bare = bareModelName(rawId);
202
+ const existingId = byBareName.get(bare);
203
+ if (existingId) {
204
+ // Same model served by both providers → alias it onto the existing
205
+ // entry so either id routes to the full serving key pool.
206
+ const entryRec = map[existingId];
207
+ const aliases = Array.isArray(entryRec?.aliases)
208
+ ? entryRec.aliases
209
+ : [];
210
+ if (!aliases.includes(rawId))
211
+ aliases.push(rawId);
212
+ entryRec.aliases = aliases;
213
+ continue;
214
+ }
215
+ if (provider.modelAffinity(rawId) !== 'yes')
216
+ continue; // not uniquely claimable
217
+ const suffixedId = `${rawId}${PROVIDER_MODEL_SUFFIX}${provider.id}`;
218
+ if (existingIds.has(suffixedId))
219
+ continue;
220
+ existingIds.add(suffixedId);
221
+ byBareName.set(bare, suffixedId);
222
+ map[suffixedId] = providerCatalogToCanonical(suffixedId, entry, countServingKeys(config, bare, provider.id));
223
+ }
224
+ }));
225
+ }
52
226
  /**
53
227
  * Build the canonical model map (id → opencode-shaped entry).
54
228
  *
55
229
  * Live path (config provided, not offline): fetch upstream model IDs and
56
- * enrich each with models.dev metadata via buildMinimalStub. Falls back to
57
- * the bundled models.json when the upstream fetch yields nothing.
230
+ * enrich each with models.dev metadata via buildMinimalStub, then append
231
+ * provider-specific catalog models (@commandcode etc.). Falls back to the
232
+ * bundled models.json when the upstream fetch yields nothing.
58
233
  * Offline path (or no config): bundled models.json only.
59
234
  */
60
235
  export async function buildCanonicalModels(config, opts) {
@@ -71,8 +246,23 @@ export async function buildCanonicalModels(config, opts) {
71
246
  const meta = await fetchModelsDevMetadata();
72
247
  if (ids && ids.length > 0) {
73
248
  const map = {};
249
+ const seen = new Set();
74
250
  for (const id of ids) {
75
251
  map[id] = buildMinimalStub(id, meta ?? undefined);
252
+ seen.add(id);
253
+ }
254
+ await appendProviderCatalogModels(map, seen, config);
255
+ // Rotation-width annotation on the primary (opencode-go) entries:
256
+ // "DeepSeek V4 Flash (3)" = 3 keys can serve this model.
257
+ for (const [id, entry] of Object.entries(map)) {
258
+ if (id.includes(PROVIDER_MODEL_SUFFIX))
259
+ continue;
260
+ if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
261
+ const rec = entry;
262
+ if (typeof rec.name === 'string') {
263
+ rec.name = annotateName(rec.name, countServingKeys(config, bareModelName(id), 'opencode-go'));
264
+ }
265
+ }
76
266
  }
77
267
  return map;
78
268
  }
@@ -1 +1 @@
1
- {"version":3,"file":"harness-models.js","sourceRoot":"","sources":["../../src/cli/harness-models.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"harness-models.js","sourceRoot":"","sources":["../../src/cli/harness-models.ts"],"names":[],"mappings":"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"}
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import type { ProxyConfig } from '../config.js';
11
11
  import { type OpencodeConfigResult } from './opencode-config.js';
12
- export type HarnessId = 'opencode' | 'pi' | 'omp';
12
+ export type HarnessId = 'opencode' | 'pi' | 'omp' | 'dsh';
13
13
  export interface HarnessSyncResult {
14
14
  harness: HarnessId;
15
15
  result: OpencodeConfigResult;
@@ -28,19 +28,36 @@ export declare function getHarnessSettingsPath(): string;
28
28
  export declare function readHarnessSettings(): HarnessId[];
29
29
  /** Write the enabled harness ids (replaces any previous selection). */
30
30
  export declare function writeHarnessSettings(ids: HarnessId[]): void;
31
- /** Map CLI harness-name args to canonical ids; unknown names land in errors. */
32
- export declare function parseHarnessArgs(args: string[]): {
33
- ids: HarnessId[];
31
+ export interface HarnessCommandArgs {
32
+ add: HarnessId[];
33
+ remove: HarnessId[];
34
+ clear: boolean;
34
35
  errors: string[];
35
- };
36
+ }
37
+ /**
38
+ * Parse `configharness` CLI args into an add/remove/clear operation.
39
+ *
40
+ * Harness names before `--remove` are added to the selection, names after it
41
+ * are removed; `--clear` alone empties the selection. Unknown names land in
42
+ * `errors` (the caller reports them and exits non-zero).
43
+ */
44
+ export declare function parseHarnessCommandArgs(args: string[]): HarnessCommandArgs;
45
+ /**
46
+ * Apply an add/remove/clear operation to the harness selection.
47
+ *
48
+ * Additive by design: names are merged into the current selection, so
49
+ * `configharness omp` followed by `configharness pi` enables both. A missing
50
+ * settings file starts from an empty selection on the first explicit command
51
+ * (the implicit opencode default only applies while nothing is configured).
52
+ *
53
+ * @returns the resulting selection after the write.
54
+ */
55
+ export declare function updateHarnessSettings(op: Omit<HarnessCommandArgs, 'errors'>): HarnessId[];
36
56
  /**
37
57
  * Sync models to every enabled harness (opt-in via the settings file).
38
58
  *
39
59
  * @param config — Proxy config; required for the live (upstream) opencode
40
- * sync. When undefined, opencode falls back to the bundled fast-path.
41
- * @param opts.offline — Skip network: use bundled models.json everywhere.
60
+ * sync. When undefined, harnesses fall back to the bundled model list.
42
61
  * @returns One result per enabled harness, in the order opencode, pi, omp.
43
62
  */
44
- export declare function syncModelsInAllHarnesses(config?: ProxyConfig, opts?: {
45
- offline?: boolean;
46
- }): Promise<HarnessSyncResult[]>;
63
+ export declare function syncModelsInAllHarnesses(config?: ProxyConfig): Promise<HarnessSyncResult[]>;
@@ -14,8 +14,9 @@ import { syncModelsToOpencodeConfig } from './opencode-config.js';
14
14
  import { syncOpencodeModelsWithUpstream } from '../models-sync.js';
15
15
  import { syncModelsToPiConfig } from './pi-config.js';
16
16
  import { syncModelsToOmpConfig } from './omp-config.js';
17
+ import { syncModelsToDshConfig } from './dsh-config.js';
17
18
  import { buildCanonicalModels } from './harness-models.js';
18
- const VALID_HARNESS_IDS = ['opencode', 'pi', 'omp'];
19
+ const VALID_HARNESS_IDS = ['opencode', 'pi', 'omp', 'dsh'];
19
20
  /** Path to the harness-selection setting: ~/.config/saros/harnesses.json */
20
21
  export function getHarnessSettingsPath() {
21
22
  return join(homedir(), '.config', 'saros', 'harnesses.json');
@@ -55,46 +56,96 @@ const HARNESS_ALIASES = {
55
56
  pi: 'pi',
56
57
  oc: 'opencode',
57
58
  opencode: 'opencode',
59
+ dsh: 'dsh',
60
+ deepseek: 'dsh',
58
61
  };
59
- /** Map CLI harness-name args to canonical ids; unknown names land in errors. */
60
- export function parseHarnessArgs(args) {
61
- const ids = [];
62
+ /**
63
+ * Parse `configharness` CLI args into an add/remove/clear operation.
64
+ *
65
+ * Harness names before `--remove` are added to the selection, names after it
66
+ * are removed; `--clear` alone empties the selection. Unknown names land in
67
+ * `errors` (the caller reports them and exits non-zero).
68
+ */
69
+ export function parseHarnessCommandArgs(args) {
70
+ const add = [];
71
+ const remove = [];
62
72
  const errors = [];
73
+ let clear = false;
74
+ let removing = false;
63
75
  for (const arg of args) {
64
- const mapped = HARNESS_ALIASES[arg.toLowerCase()];
65
- if (mapped) {
66
- if (!ids.includes(mapped))
67
- ids.push(mapped);
76
+ if (arg === '--clear') {
77
+ clear = true;
78
+ continue;
68
79
  }
69
- else {
80
+ if (arg === '--remove') {
81
+ removing = true;
82
+ continue;
83
+ }
84
+ const mapped = HARNESS_ALIASES[arg.toLowerCase()];
85
+ if (!mapped) {
70
86
  errors.push(arg);
87
+ continue;
71
88
  }
89
+ const target = removing ? remove : add;
90
+ if (!target.includes(mapped))
91
+ target.push(mapped);
72
92
  }
73
- return { ids, errors };
93
+ return { add, remove, clear, errors };
94
+ }
95
+ /**
96
+ * Apply an add/remove/clear operation to the harness selection.
97
+ *
98
+ * Additive by design: names are merged into the current selection, so
99
+ * `configharness omp` followed by `configharness pi` enables both. A missing
100
+ * settings file starts from an empty selection on the first explicit command
101
+ * (the implicit opencode default only applies while nothing is configured).
102
+ *
103
+ * @returns the resulting selection after the write.
104
+ */
105
+ export function updateHarnessSettings(op) {
106
+ if (op.clear) {
107
+ writeHarnessSettings([]);
108
+ return [];
109
+ }
110
+ const fileExists = existsSync(getHarnessSettingsPath());
111
+ if (!fileExists && op.add.length === 0) {
112
+ // Nothing configured yet and nothing to add — keep the implicit
113
+ // opencode default instead of writing an explicit empty list.
114
+ return readHarnessSettings();
115
+ }
116
+ const base = fileExists ? readHarnessSettings() : [];
117
+ const next = base.filter((id) => !op.remove.includes(id));
118
+ for (const id of op.add) {
119
+ if (!next.includes(id))
120
+ next.push(id);
121
+ }
122
+ writeHarnessSettings(next);
123
+ return next;
74
124
  }
75
125
  /**
76
126
  * Sync models to every enabled harness (opt-in via the settings file).
77
127
  *
78
128
  * @param config — Proxy config; required for the live (upstream) opencode
79
- * sync. When undefined, opencode falls back to the bundled fast-path.
80
- * @param opts.offline — Skip network: use bundled models.json everywhere.
129
+ * sync. When undefined, harnesses fall back to the bundled model list.
81
130
  * @returns One result per enabled harness, in the order opencode, pi, omp.
82
131
  */
83
- export async function syncModelsInAllHarnesses(config, opts = {}) {
132
+ export async function syncModelsInAllHarnesses(config) {
84
133
  const enabled = readHarnessSettings();
85
134
  if (enabled.length === 0)
86
135
  return [];
87
136
  const results = [];
88
137
  if (enabled.includes('opencode')) {
89
- if (opts.offline || !config) {
138
+ if (!config) {
90
139
  results.push({ harness: 'opencode', result: syncModelsToOpencodeConfig() });
91
140
  }
92
141
  else {
93
142
  results.push({ harness: 'opencode', result: await syncOpencodeModelsWithUpstream(config) });
94
143
  }
95
144
  }
96
- if (enabled.includes('pi') || enabled.includes('omp')) {
97
- const modelsMap = await buildCanonicalModels(config, opts);
145
+ if (enabled.includes('pi') ||
146
+ enabled.includes('omp') ||
147
+ enabled.includes('dsh')) {
148
+ const modelsMap = await buildCanonicalModels(config);
98
149
  const port = config?.port ?? 3000;
99
150
  if (enabled.includes('pi')) {
100
151
  results.push({ harness: 'pi', result: syncModelsToPiConfig(modelsMap, port) });
@@ -102,6 +153,9 @@ export async function syncModelsInAllHarnesses(config, opts = {}) {
102
153
  if (enabled.includes('omp')) {
103
154
  results.push({ harness: 'omp', result: syncModelsToOmpConfig(modelsMap, port) });
104
155
  }
156
+ if (enabled.includes('dsh')) {
157
+ results.push({ harness: 'dsh', result: syncModelsToDshConfig(modelsMap, port) });
158
+ }
105
159
  }
106
160
  return results;
107
161
  }