agentsw 0.6.0 → 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,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, stripApiVersion } 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: stripApiVersion(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,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, home, localAppDataDir, readTextIfExists, writeFileAtomic } from "../fsutil.js";
5
- import { stripApiVersion } from "./wire.js";
5
+ import { sdkBaseUrl } from "./wire.js";
6
6
  function hermesHome() {
7
7
  const env = process.env.HERMES_HOME?.trim();
8
8
  if (env)
@@ -47,9 +47,13 @@ export const hermes = {
47
47
  }
48
48
  const entry = {
49
49
  name: provider.name,
50
- api: stripApiVersion(provider.baseUrl),
50
+ api: sdkBaseUrl(provider.protocol, provider.baseUrl),
51
51
  key_env: keyVar,
52
- 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",
53
57
  default_model: provider.defaultModel,
54
58
  models,
55
59
  };
@@ -158,9 +162,14 @@ export const hermes = {
158
162
  return Object.entries(parsed.providers).flatMap(([id, entry]) => {
159
163
  if (!entry || typeof entry.api !== "string" || !entry.api)
160
164
  return [];
161
- 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;
162
170
  if (!protocol)
163
171
  return [];
172
+ const openaiApi = protocol === "openai" ? (entry.transport === "codex_responses" ? "responses" : "completions") : undefined;
164
173
  const keyEnv = typeof entry.key_env === "string" && entry.key_env ? entry.key_env : undefined;
165
174
  const apiKey = keyEnv ? readEnv(keyEnv) : undefined;
166
175
  const models = Object.keys(entry.models ?? {});
@@ -168,7 +177,7 @@ export const hermes = {
168
177
  const defaultModel = entry.default_model ?? active;
169
178
  if (defaultModel && !models.includes(defaultModel))
170
179
  models.unshift(defaultModel);
171
- 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 }];
172
181
  });
173
182
  },
174
183
  };
@@ -3,8 +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 { stripApiVersion } from "./wire.js";
7
- import { apiValue, classifyApi, entryApi, mergeModels, stripConflictingOverrides } from "./wire.js";
6
+ import { apiValue, classifyApi, entryApi, mergeModels, sdkBaseUrl, stripConflictingOverrides } from "./wire.js";
8
7
  /** Per-model keys this adapter writes; one that stops being emitted is cleared, not inherited. */
9
8
  const OWNED_MODEL_KEYS = ["id", "name", "reasoning", "input", "contextWindow", "maxTokens", "cost"];
10
9
  const agentDir = path.join(home, ".omp", "agent");
@@ -34,6 +33,7 @@ export const omp = {
34
33
  const prev = doc.getIn(["providers", provider.id])?.toJSON?.();
35
34
  const anthropic = provider.protocol === "anthropic";
36
35
  const api = apiValue(provider.protocol, provider.openaiApi, entryApi(prev ?? {}));
36
+ const baseUrl = sdkBaseUrl(provider.protocol, provider.baseUrl);
37
37
  const models = mergeModels(prev?.models, provider.models.map((m) => ({
38
38
  id: m.id,
39
39
  ...(m.name ? { name: m.name } : {}),
@@ -52,11 +52,11 @@ export const omp = {
52
52
  }
53
53
  : {}),
54
54
  })), OWNED_MODEL_KEYS);
55
- const conflicts = stripConflictingOverrides(models, api, provider.baseUrl);
55
+ const conflicts = stripConflictingOverrides(models, api, baseUrl);
56
56
  if (conflicts.length)
57
57
  notes.push(`dropped model overrides pointing elsewhere: ${conflicts.join(", ")}`);
58
58
  const entry = {
59
- baseUrl: stripApiVersion(provider.baseUrl),
59
+ baseUrl,
60
60
  apiKey: provider.apiKey, // omp treats value as env-var name first, then literal
61
61
  api,
62
62
  models,
@@ -1,9 +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
- import { stripApiVersion } from "./wire.js";
5
- const configDir = process.env.OPENCODE_CONFIG_DIR?.trim() || appDataDir("opencode");
6
- 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");
7
8
  /**
8
9
  * opencode: custom providers in ~/.config/opencode/opencode.json "provider" map,
9
10
  * backed by @ai-sdk/openai-compatible or @ai-sdk/anthropic npm loaders.
@@ -48,11 +49,11 @@ export const opencode = {
48
49
  },
49
50
  }
50
51
  : {}),
51
- ...(m.contextWindow || m.maxOutput
52
+ ...(m.contextWindow && m.maxOutput
52
53
  ? {
53
54
  limit: {
54
- ...(m.contextWindow ? { context: m.contextWindow } : {}),
55
- ...(m.maxOutput ? { output: m.maxOutput } : {}),
55
+ context: m.contextWindow,
56
+ output: m.maxOutput,
56
57
  },
57
58
  }
58
59
  : {}),
@@ -60,11 +61,11 @@ export const opencode = {
60
61
  }
61
62
  providerMap[provider.id] = {
62
63
  ...prev,
63
- 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",
64
65
  name: provider.name,
65
66
  options: {
66
67
  ...prev.options,
67
- baseURL: stripApiVersion(provider.baseUrl),
68
+ baseURL: provider.baseUrl.replace(/\/+$/, ""),
68
69
  apiKey: provider.apiKey,
69
70
  },
70
71
  models,
@@ -120,6 +121,7 @@ export const opencode = {
120
121
  if (!entry.npm)
121
122
  return []; // built-in provider without a custom npm loader
122
123
  const protocol = entry.npm.includes("anthropic") ? "anthropic" : "openai";
124
+ const openaiApi = protocol === "openai" ? (entry.npm === "@ai-sdk/openai" ? "responses" : "completions") : undefined;
123
125
  let apiKey = entry.options?.apiKey;
124
126
  let keyEnv;
125
127
  const ref = apiKey?.match(/^\{env:([A-Za-z0-9_]+)\}$/);
@@ -131,7 +133,7 @@ export const opencode = {
131
133
  const defaultModel = activeProvider === id ? activeModel : undefined;
132
134
  if (defaultModel && !models.includes(defaultModel))
133
135
  models.unshift(defaultModel);
134
- 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 }];
135
137
  });
136
138
  },
137
139
  };
@@ -2,8 +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 { stripApiVersion } from "./wire.js";
6
- import { apiValue, classifyApi, entryApi, mergeModels, stripConflictingOverrides } from "./wire.js";
5
+ import { apiValue, classifyApi, entryApi, mergeModels, sdkBaseUrl, stripConflictingOverrides } from "./wire.js";
7
6
  /** Per-model keys this adapter writes; one that stops being emitted is cleared, not inherited. */
8
7
  const OWNED_MODEL_KEYS = [
9
8
  "id",
@@ -60,6 +59,7 @@ export function piStyleTarget(opts) {
60
59
  ? existing
61
60
  : {});
62
61
  const api = apiValue(provider.protocol, provider.openaiApi, entryApi(prev));
62
+ const baseUrl = sdkBaseUrl(provider.protocol, provider.baseUrl);
63
63
  const models = mergeModels(prev.models, provider.models.map((m) => ({
64
64
  id: m.id,
65
65
  ...(m.name ? { name: m.name } : {}),
@@ -81,13 +81,13 @@ export function piStyleTarget(opts) {
81
81
  }
82
82
  : {}),
83
83
  })), OWNED_MODEL_KEYS);
84
- const conflicts = stripConflictingOverrides(models, api, provider.baseUrl);
84
+ const conflicts = stripConflictingOverrides(models, api, baseUrl);
85
85
  if (conflicts.length)
86
86
  notes.push(`dropped model overrides pointing elsewhere: ${conflicts.join(", ")}`);
87
87
  providers[provider.id] = {
88
88
  ...prev,
89
89
  name: provider.name,
90
- baseUrl: stripApiVersion(provider.baseUrl),
90
+ baseUrl,
91
91
  apiKey: provider.apiKey, // literal key; pi treats "$VAR"/"!cmd" as indirection, prime also accepts bare env names
92
92
  api,
93
93
  models,
@@ -108,11 +108,14 @@ export function stripConflictingOverrides(models, api, baseUrl) {
108
108
  return dropped;
109
109
  }
110
110
  /**
111
- * Strip a trailing API version segment (e.g. `/v1`, `/v2beta`) from a base URL.
112
- * Use this when writing to apps whose own SDK appends `/v1/...` to the base URL
113
- * (Anthropic SDK, OpenAI SDK, omp, pi, hermes, dsh). Codex is the exception —
114
- * it appends `/responses` directly, so it keeps the `/v1` segment.
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`.
115
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`). */
116
119
  export function stripApiVersion(baseUrl) {
117
120
  return baseUrl.replace(/\/v\d+(?:beta\d*)?\/?$/i, "");
118
121
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentsw",
3
- "version": "0.6.0",
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": {