agentsw 0.5.5 → 0.6.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.
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { backupFile, home, readJsonIfExists, writeFileAtomic } from "../fsutil.js";
4
4
  import { slugFromBaseUrl } from "../slug.js";
5
+ import { stripApiVersion } from "./wire.js";
5
6
  const dir = path.join(home, ".claude");
6
7
  const settingsFile = path.join(dir, "settings.json");
7
8
  /**
@@ -18,7 +19,7 @@ export const claudecode = {
18
19
  const notes = [];
19
20
  const settings = readJsonIfExists(settingsFile) ?? {};
20
21
  const env = { ...settings.env };
21
- env.ANTHROPIC_BASE_URL = provider.baseUrl;
22
+ env.ANTHROPIC_BASE_URL = stripApiVersion(provider.baseUrl);
22
23
  env.ANTHROPIC_AUTH_TOKEN = provider.apiKey;
23
24
  delete env.ANTHROPIC_API_KEY; // AUTH_TOKEN and API_KEY conflict; keep one
24
25
  env.ANTHROPIC_MODEL = provider.defaultModel;
@@ -38,7 +39,7 @@ export const claudecode = {
38
39
  async prune(provider) {
39
40
  const settings = readJsonIfExists(settingsFile);
40
41
  const env = settings?.env;
41
- if (!settings || !env || env.ANTHROPIC_BASE_URL !== provider.baseUrl) {
42
+ if (!settings || !env || env.ANTHROPIC_BASE_URL !== stripApiVersion(provider.baseUrl)) {
42
43
  return { app: this.id, changed: [], notes: [], skipped: "claude env does not point at this provider" };
43
44
  }
44
45
  for (const key of [
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import YAML from "yaml";
4
4
  import { backupFile, expandHome, home, localAppDataDir, readTextIfExists, writeFileAtomic } from "../fsutil.js";
5
- import { apiValue, classifyApi, mergeModels } from "./wire.js";
5
+ import { apiValue, classifyApi, mergeModels, sdkBaseUrl } from "./wire.js";
6
6
  /** `$DSH_HOME`, or `~/.dsh` (packages/util/home-paths `resolveDshHome`). */
7
7
  function dshHome() {
8
8
  const env = process.env.DSH_HOME?.trim();
@@ -91,7 +91,7 @@ export const dsh = {
91
91
  displayName: provider.name,
92
92
  apiKeyEnv: ref, // credential reference, never the secret
93
93
  api: apiValue(provider.protocol, provider.openaiApi, prev?.api),
94
- baseURL: provider.baseUrl,
94
+ baseURL: sdkBaseUrl(provider.protocol, provider.baseUrl),
95
95
  models: mergeModels(prev?.models, provider.models.map(modelEntry), OWNED_MODEL_KEYS),
96
96
  };
97
97
  if (YAML.isMap(doc.getIn(at))) {
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import YAML from "yaml";
4
4
  import { backupFile, home, localAppDataDir, readTextIfExists, writeFileAtomic } from "../fsutil.js";
5
+ import { sdkBaseUrl } from "./wire.js";
5
6
  function hermesHome() {
6
7
  const env = process.env.HERMES_HOME?.trim();
7
8
  if (env)
@@ -46,9 +47,13 @@ export const hermes = {
46
47
  }
47
48
  const entry = {
48
49
  name: provider.name,
49
- api: provider.baseUrl,
50
+ api: sdkBaseUrl(provider.protocol, provider.baseUrl),
50
51
  key_env: keyVar,
51
- transport: provider.protocol === "anthropic" ? "anthropic_messages" : "chat_completions",
52
+ transport: provider.protocol === "anthropic"
53
+ ? "anthropic_messages"
54
+ : provider.openaiApi === "responses"
55
+ ? "codex_responses"
56
+ : "chat_completions",
52
57
  default_model: provider.defaultModel,
53
58
  models,
54
59
  };
@@ -157,9 +162,14 @@ export const hermes = {
157
162
  return Object.entries(parsed.providers).flatMap(([id, entry]) => {
158
163
  if (!entry || typeof entry.api !== "string" || !entry.api)
159
164
  return [];
160
- const protocol = entry.transport === "anthropic_messages" ? "anthropic" : entry.transport === "chat_completions" ? "openai" : undefined;
165
+ const protocol = entry.transport === "anthropic_messages"
166
+ ? "anthropic"
167
+ : ["chat_completions", "codex_responses"].includes(entry.transport ?? "")
168
+ ? "openai"
169
+ : undefined;
161
170
  if (!protocol)
162
171
  return [];
172
+ const openaiApi = protocol === "openai" ? (entry.transport === "codex_responses" ? "responses" : "completions") : undefined;
163
173
  const keyEnv = typeof entry.key_env === "string" && entry.key_env ? entry.key_env : undefined;
164
174
  const apiKey = keyEnv ? readEnv(keyEnv) : undefined;
165
175
  const models = Object.keys(entry.models ?? {});
@@ -167,7 +177,7 @@ export const hermes = {
167
177
  const defaultModel = entry.default_model ?? active;
168
178
  if (defaultModel && !models.includes(defaultModel))
169
179
  models.unshift(defaultModel);
170
- return [{ id, name: entry.name ?? id, protocol, baseUrl: entry.api, apiKey, keyEnv, models, defaultModel, source: self }];
180
+ return [{ id, name: entry.name ?? id, protocol, openaiApi, baseUrl: entry.api, apiKey, keyEnv, models, defaultModel, source: self }];
171
181
  });
172
182
  },
173
183
  };
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
  import YAML from "yaml";
4
4
  import { backupFile, home, readTextIfExists, writeFileAtomic } from "../fsutil.js";
5
5
  import { looksLikeEnvName } from "../slug.js";
6
- import { apiValue, classifyApi, entryApi, mergeModels, stripConflictingOverrides } from "./wire.js";
6
+ import { apiValue, classifyApi, entryApi, mergeModels, sdkBaseUrl, stripConflictingOverrides } from "./wire.js";
7
7
  /** Per-model keys this adapter writes; one that stops being emitted is cleared, not inherited. */
8
8
  const OWNED_MODEL_KEYS = ["id", "name", "reasoning", "input", "contextWindow", "maxTokens", "cost"];
9
9
  const agentDir = path.join(home, ".omp", "agent");
@@ -33,6 +33,7 @@ export const omp = {
33
33
  const prev = doc.getIn(["providers", provider.id])?.toJSON?.();
34
34
  const anthropic = provider.protocol === "anthropic";
35
35
  const api = apiValue(provider.protocol, provider.openaiApi, entryApi(prev ?? {}));
36
+ const baseUrl = sdkBaseUrl(provider.protocol, provider.baseUrl);
36
37
  const models = mergeModels(prev?.models, provider.models.map((m) => ({
37
38
  id: m.id,
38
39
  ...(m.name ? { name: m.name } : {}),
@@ -51,11 +52,11 @@ export const omp = {
51
52
  }
52
53
  : {}),
53
54
  })), OWNED_MODEL_KEYS);
54
- const conflicts = stripConflictingOverrides(models, api, provider.baseUrl);
55
+ const conflicts = stripConflictingOverrides(models, api, baseUrl);
55
56
  if (conflicts.length)
56
57
  notes.push(`dropped model overrides pointing elsewhere: ${conflicts.join(", ")}`);
57
58
  const entry = {
58
- baseUrl: provider.baseUrl,
59
+ baseUrl,
59
60
  apiKey: provider.apiKey, // omp treats value as env-var name first, then literal
60
61
  api,
61
62
  models,
@@ -1,8 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { appDataDir, backupFile, readJsonIfExists, writeFileAtomic } from "../fsutil.js";
4
- const configDir = process.env.OPENCODE_CONFIG_DIR?.trim() || appDataDir("opencode");
5
- const configFile = path.join(configDir, "opencode.json");
3
+ import { appDataDir, backupFile, expandHome, readJsonIfExists, writeFileAtomic } from "../fsutil.js";
4
+ const customDir = process.env.OPENCODE_CONFIG_DIR?.trim();
5
+ const configFile = customDir
6
+ ? path.join(expandHome(customDir), "opencode.json")
7
+ : path.join(appDataDir("opencode"), "opencode.json");
6
8
  /**
7
9
  * opencode: custom providers in ~/.config/opencode/opencode.json "provider" map,
8
10
  * backed by @ai-sdk/openai-compatible or @ai-sdk/anthropic npm loaders.
@@ -47,11 +49,11 @@ export const opencode = {
47
49
  },
48
50
  }
49
51
  : {}),
50
- ...(m.contextWindow || m.maxOutput
52
+ ...(m.contextWindow && m.maxOutput
51
53
  ? {
52
54
  limit: {
53
- ...(m.contextWindow ? { context: m.contextWindow } : {}),
54
- ...(m.maxOutput ? { output: m.maxOutput } : {}),
55
+ context: m.contextWindow,
56
+ output: m.maxOutput,
55
57
  },
56
58
  }
57
59
  : {}),
@@ -59,11 +61,11 @@ export const opencode = {
59
61
  }
60
62
  providerMap[provider.id] = {
61
63
  ...prev,
62
- npm: anthropic ? "@ai-sdk/anthropic" : "@ai-sdk/openai-compatible",
64
+ npm: anthropic ? "@ai-sdk/anthropic" : provider.openaiApi === "responses" ? "@ai-sdk/openai" : "@ai-sdk/openai-compatible",
63
65
  name: provider.name,
64
66
  options: {
65
67
  ...prev.options,
66
- baseURL: provider.baseUrl,
68
+ baseURL: provider.baseUrl.replace(/\/+$/, ""),
67
69
  apiKey: provider.apiKey,
68
70
  },
69
71
  models,
@@ -119,6 +121,7 @@ export const opencode = {
119
121
  if (!entry.npm)
120
122
  return []; // built-in provider without a custom npm loader
121
123
  const protocol = entry.npm.includes("anthropic") ? "anthropic" : "openai";
124
+ const openaiApi = protocol === "openai" ? (entry.npm === "@ai-sdk/openai" ? "responses" : "completions") : undefined;
122
125
  let apiKey = entry.options?.apiKey;
123
126
  let keyEnv;
124
127
  const ref = apiKey?.match(/^\{env:([A-Za-z0-9_]+)\}$/);
@@ -130,7 +133,7 @@ export const opencode = {
130
133
  const defaultModel = activeProvider === id ? activeModel : undefined;
131
134
  if (defaultModel && !models.includes(defaultModel))
132
135
  models.unshift(defaultModel);
133
- return [{ id, name: entry.name ?? id, protocol, baseUrl, apiKey, keyEnv, models, defaultModel, source: self }];
136
+ return [{ id, name: entry.name ?? id, protocol, openaiApi, baseUrl, apiKey, keyEnv, models, defaultModel, source: self }];
134
137
  });
135
138
  },
136
139
  };
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { backupFile, home, readJsonIfExists, writeFileAtomic } from "../fsutil.js";
4
4
  import { looksLikeEnvName } from "../slug.js";
5
- import { apiValue, classifyApi, entryApi, mergeModels, stripConflictingOverrides } from "./wire.js";
5
+ import { apiValue, classifyApi, entryApi, mergeModels, sdkBaseUrl, stripConflictingOverrides } from "./wire.js";
6
6
  /** Per-model keys this adapter writes; one that stops being emitted is cleared, not inherited. */
7
7
  const OWNED_MODEL_KEYS = [
8
8
  "id",
@@ -59,6 +59,7 @@ export function piStyleTarget(opts) {
59
59
  ? existing
60
60
  : {});
61
61
  const api = apiValue(provider.protocol, provider.openaiApi, entryApi(prev));
62
+ const baseUrl = sdkBaseUrl(provider.protocol, provider.baseUrl);
62
63
  const models = mergeModels(prev.models, provider.models.map((m) => ({
63
64
  id: m.id,
64
65
  ...(m.name ? { name: m.name } : {}),
@@ -80,13 +81,13 @@ export function piStyleTarget(opts) {
80
81
  }
81
82
  : {}),
82
83
  })), OWNED_MODEL_KEYS);
83
- const conflicts = stripConflictingOverrides(models, api, provider.baseUrl);
84
+ const conflicts = stripConflictingOverrides(models, api, baseUrl);
84
85
  if (conflicts.length)
85
86
  notes.push(`dropped model overrides pointing elsewhere: ${conflicts.join(", ")}`);
86
87
  providers[provider.id] = {
87
88
  ...prev,
88
89
  name: provider.name,
89
- baseUrl: provider.baseUrl,
90
+ baseUrl,
90
91
  apiKey: provider.apiKey, // literal key; pi treats "$VAR"/"!cmd" as indirection, prime also accepts bare env names
91
92
  api,
92
93
  models,
@@ -107,3 +107,15 @@ export function stripConflictingOverrides(models, api, baseUrl) {
107
107
  }
108
108
  return dropped;
109
109
  }
110
+ /**
111
+ * Return the base URL expected by SDK-backed clients. Anthropic SDK methods
112
+ * append `/v1/...`, while OpenAI-compatible clients append only the operation
113
+ * path (`/responses` or `/chat/completions`) and therefore keep `/v1`.
114
+ */
115
+ export function sdkBaseUrl(protocol, baseUrl) {
116
+ return protocol === "anthropic" ? stripApiVersion(baseUrl) : baseUrl.replace(/\/+$/, "");
117
+ }
118
+ /** Strip a trailing API version segment (e.g. `/v1`, `/v2beta`). */
119
+ export function stripApiVersion(baseUrl) {
120
+ return baseUrl.replace(/\/v\d+(?:beta\d*)?\/?$/i, "");
121
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentsw",
3
- "version": "0.5.5",
3
+ "version": "0.6.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": {